diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 6c785d36f..000000000 --- a/.gitignore +++ /dev/null @@ -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 diff --git a/.gitlab-ci-files/job-android.yml b/.gitlab-ci-files/job-android.yml deleted file mode 100644 index a66ce9e9f..000000000 --- a/.gitlab-ci-files/job-android.yml +++ /dev/null @@ -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 diff --git a/.gitlab-ci-files/job-upload.yml b/.gitlab-ci-files/job-upload.yml deleted file mode 100644 index 31e1d1a50..000000000 --- a/.gitlab-ci-files/job-upload.yml +++ /dev/null @@ -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 \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 0e65baf26..000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -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 diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 796b96d1c..000000000 --- a/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 10c35e7a2..000000000 --- a/app/build.gradle +++ /dev/null @@ -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) - } -} diff --git a/app/contacts.xml b/app/contacts.xml deleted file mode 100644 index 07ea82c10..000000000 --- a/app/contacts.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/app/google-services.json b/app/google-services.json deleted file mode 100644 index c26aa7702..000000000 --- a/app/google-services.json +++ /dev/null @@ -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" -} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index e70e74af6..000000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -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.()) -#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.()) -#> Task :app:lintVitalAnalyzeRelease -#FAILURE: Build failed with an exception. --dontwarn org.conscrypt.** --dontwarn org.bouncycastle.** --dontwarn org.openjsse.** \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml deleted file mode 100644 index c1b106232..000000000 --- a/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,256 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/assets/assistant_default_values b/app/src/main/assets/assistant_default_values deleted file mode 100644 index 5800203a4..000000000 --- a/app/src/main/assets/assistant_default_values +++ /dev/null @@ -1,42 +0,0 @@ - - -
- 0 - 0 - 0 - -1 - - 0 - 0 - 3600 - - - - 1 - - - - - 0 - 0 - 0 - -
-
- - -
-
- 0 -
-
- - MD5 - -1 - 0 - -1 - 128 - 1 - ^[a-zA-Z0-9+_.\-]*$ -
-
diff --git a/app/src/main/assets/assistant_linphone_default_values b/app/src/main/assets/assistant_linphone_default_values deleted file mode 100644 index 7a72cd0c8..000000000 --- a/app/src/main/assets/assistant_linphone_default_values +++ /dev/null @@ -1,42 +0,0 @@ - - -
- 1 - 0 - 1 - 120 - sip:voip-metrics@sip.linphone.org;transport=tls - 1 - 180 - 31536000 - sip:?@sip.linphone.org - <sip:sip.linphone.org;transport=tls> - <sip:sip.linphone.org;transport=tls> - 1 - nat_policy_default_values - sip.linphone.org - sip:conference-factory@sip.linphone.org - sip:videoconference-factory@sip.linphone.org - 1 - 1 - 1 - https://lime.linphone.org/lime-server/lime-server.php -
-
- stun.linphone.org - stun,ice -
-
- 1 -
-
- sip.linphone.org - SHA-256 - -1 - 1 - -1 - 64 - 1 - ^[a-z0-9+_.\-]*$ -
-
diff --git a/app/src/main/assets/linphonerc_default b/app/src/main/assets/linphonerc_default deleted file mode 100644 index bad1a1a76..000000000 --- a/app/src/main/assets/linphonerc_default +++ /dev/null @@ -1,44 +0,0 @@ - -## Start of default rc - -[sip] -contact="Linphone Android" -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 diff --git a/app/src/main/assets/linphonerc_factory b/app/src/main/assets/linphonerc_factory deleted file mode 100644 index 23e6571e4..000000000 --- a/app/src/main/assets/linphonerc_factory +++ /dev/null @@ -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 diff --git a/app/src/main/java/org/linphone/LinphoneApplication.kt b/app/src/main/java/org/linphone/LinphoneApplication.kt deleted file mode 100644 index 2a08ce958..000000000 --- a/app/src/main/java/org/linphone/LinphoneApplication.kt +++ /dev/null @@ -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 . - */ -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() - } -} diff --git a/app/src/main/java/org/linphone/activities/GenericActivity.kt b/app/src/main/java/org/linphone/activities/GenericActivity.kt deleted file mode 100644 index 4498c8696..000000000 --- a/app/src/main/java/org/linphone/activities/GenericActivity.kt +++ /dev/null @@ -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 . - */ -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) - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/GenericFragment.kt b/app/src/main/java/org/linphone/activities/GenericFragment.kt deleted file mode 100644 index ab8db20ef..000000000 --- a/app/src/main/java/org/linphone/activities/GenericFragment.kt +++ /dev/null @@ -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 . - */ -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 : 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(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 - } -} diff --git a/app/src/main/java/org/linphone/activities/Navigation.kt b/app/src/main/java/org/linphone/activities/Navigation.kt deleted file mode 100644 index c03d854d1..000000000 --- a/app/src/main/java/org/linphone/activities/Navigation.kt +++ /dev/null @@ -1,1222 +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 . - */ -package org.linphone.activities - -import android.net.Uri -import android.os.Bundle -import androidx.core.os.bundleOf -import androidx.fragment.app.Fragment -import androidx.navigation.NavController -import androidx.navigation.NavOptions -import androidx.navigation.findNavController -import androidx.navigation.fragment.NavHostFragment -import androidx.navigation.fragment.findNavController -import androidx.slidingpanelayout.widget.SlidingPaneLayout -import org.linphone.R -import org.linphone.activities.assistant.fragments.* -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.chat.fragments.ChatRoomCreationFragment -import org.linphone.activities.main.chat.fragments.DetailChatRoomFragment -import org.linphone.activities.main.chat.fragments.GroupInfoFragment -import org.linphone.activities.main.chat.fragments.MasterChatRoomsFragment -import org.linphone.activities.main.conference.fragments.* -import org.linphone.activities.main.contact.fragments.ContactEditorFragment -import org.linphone.activities.main.contact.fragments.DetailContactFragment -import org.linphone.activities.main.contact.fragments.MasterContactsFragment -import org.linphone.activities.main.dialer.fragments.DialerFragment -import org.linphone.activities.main.fragments.TabsFragment -import org.linphone.activities.main.history.fragments.DetailCallLogFragment -import org.linphone.activities.main.history.fragments.MasterCallLogsFragment -import org.linphone.activities.main.settings.fragments.* -import org.linphone.activities.main.sidemenu.fragments.SideMenuFragment -import org.linphone.activities.voip.CallActivity -import org.linphone.activities.voip.fragments.* -import org.linphone.core.tools.Log - -internal fun Fragment.findMasterNavController(): NavController { - return parentFragment?.parentFragment?.findNavController() ?: findNavController() -} - -fun popupTo( - popUpTo: Int = -1, - popUpInclusive: Boolean = false, - singleTop: Boolean = true -): NavOptions { - val builder = NavOptions.Builder() - builder.setPopUpTo(popUpTo, popUpInclusive).setLaunchSingleTop(singleTop) - return builder.build() -} - -/* Main activity related */ - -internal fun MainActivity.navigateToDialer(args: Bundle? = null) { - findNavController(R.id.nav_host_fragment).navigate( - R.id.action_global_dialerFragment, - args, - popupTo(R.id.dialerFragment, true) - ) -} - -internal fun MainActivity.navigateToChatRooms(args: Bundle? = null) { - findNavController(R.id.nav_host_fragment).navigate( - R.id.action_global_masterChatRoomsFragment, - args, - popupTo(R.id.masterChatRoomsFragment, true) - ) -} - -internal fun MainActivity.navigateToChatRoom(localAddress: String?, peerAddress: String?) { - val deepLink = "linphone-android://chat-room/$localAddress/$peerAddress" - findNavController(R.id.nav_host_fragment).navigate( - Uri.parse(deepLink), - popupTo(R.id.masterChatRoomsFragment, true) - ) -} - -internal fun MainActivity.navigateToContacts() { - findNavController(R.id.nav_host_fragment).navigate( - R.id.action_global_masterContactsFragment, - null, - popupTo(R.id.masterContactsFragment, true) - ) -} - -internal fun MainActivity.navigateToContact(contactId: String?) { - val deepLink = "linphone-android://contact/view/$contactId" - findNavController(R.id.nav_host_fragment).navigate( - Uri.parse(deepLink), - popupTo(R.id.masterContactsFragment, true) - ) -} - -/* Tabs fragment related */ - -internal fun TabsFragment.navigateToCallHistory() { - val action = when (findNavController().currentDestination?.id) { - R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterCallLogsFragment - R.id.dialerFragment -> R.id.action_dialerFragment_to_masterCallLogsFragment - R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterCallLogsFragment - else -> R.id.action_global_masterCallLogsFragment - } - findNavController().navigate( - action, - null, - popupTo(R.id.masterCallLogsFragment, true) - ) -} - -internal fun TabsFragment.navigateToContacts() { - val action = when (findNavController().currentDestination?.id) { - R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterContactsFragment - R.id.dialerFragment -> R.id.action_dialerFragment_to_masterContactsFragment - R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_masterContactsFragment - else -> R.id.action_global_masterContactsFragment - } - findNavController().navigate( - action, - null, - popupTo(R.id.masterContactsFragment, true) - ) -} - -internal fun TabsFragment.navigateToDialer() { - val action = when (findNavController().currentDestination?.id) { - R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_dialerFragment - R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_dialerFragment - R.id.masterChatRoomsFragment -> R.id.action_masterChatRoomsFragment_to_dialerFragment - else -> R.id.action_global_dialerFragment - } - findNavController().navigate( - action, - null, - popupTo(R.id.dialerFragment, true) - ) -} - -internal fun TabsFragment.navigateToChatRooms() { - val action = when (findNavController().currentDestination?.id) { - R.id.masterCallLogsFragment -> R.id.action_masterCallLogsFragment_to_masterChatRoomsFragment - R.id.masterContactsFragment -> R.id.action_masterContactsFragment_to_masterChatRoomsFragment - R.id.dialerFragment -> R.id.action_dialerFragment_to_masterChatRoomsFragment - else -> R.id.action_global_masterChatRoomsFragment - } - findNavController().navigate( - action, - null, - popupTo(R.id.masterChatRoomsFragment, true) - ) -} - -/* Dialer related */ - -internal fun DialerFragment.navigateToContacts(uriToAdd: String?) { - if (uriToAdd.isNullOrEmpty()) { - Log.e("[Navigation] SIP URI to add to contact is null or empty!") - return - } - - val deepLink = "linphone-android://contact/new/$uriToAdd" - findNavController().navigate( - Uri.parse(deepLink), - popupTo(R.id.masterContactsFragment, true) - ) -} - -internal fun DialerFragment.navigateToConfigFileViewer() { - val bundle = bundleOf("Secure" to true) - findMasterNavController().navigate( - R.id.action_global_configViewerFragment, - bundle, - popupTo() - ) -} - -internal fun DialerFragment.navigateToConferenceScheduling() { - findMasterNavController().navigate( - R.id.action_global_conferenceSchedulingFragment, - null, - popupTo() - ) -} - -/* Conference scheduling related */ - -internal fun ConferenceSchedulingFragment.navigateToParticipantsList() { - if (findNavController().currentDestination?.id == R.id.conferenceSchedulingFragment) { - findNavController().navigate( - R.id.action_conferenceSchedulingFragment_to_conferenceSchedulingParticipantsListFragment, - null, - popupTo(R.id.conferenceSchedulingParticipantsListFragment, true) - ) - } -} - -internal fun ConferenceSchedulingParticipantsListFragment.navigateToSummary() { - if (findNavController().currentDestination?.id == R.id.conferenceSchedulingParticipantsListFragment) { - findNavController().navigate( - R.id.action_conferenceSchedulingParticipantsListFragment_to_conferenceSchedulingSummaryFragment, - null, - popupTo(R.id.conferenceSchedulingSummaryFragment, true) - ) - } -} - -internal fun ConferenceSchedulingSummaryFragment.navigateToScheduledConferences() { - if (findNavController().currentDestination?.id == R.id.conferenceSchedulingSummaryFragment) { - findNavController().navigate( - R.id.action_global_scheduledConferencesFragment, - null, - popupTo(R.id.dialerFragment, false) - ) - } -} - -internal fun ConferenceSchedulingSummaryFragment.navigateToDialer() { - val bundle = Bundle() - findMasterNavController().navigate( - R.id.action_global_dialerFragment, - bundle, - popupTo(R.id.dialerFragment, false) - ) -} - -internal fun DetailChatRoomFragment.navigateToConferenceWaitingRoom( - address: String, - subject: String? -) { - val bundle = Bundle() - bundle.putString("Address", address) - bundle.putString("Subject", subject) - findMasterNavController().navigate( - R.id.action_global_conferenceWaitingRoomFragment, - bundle, - popupTo(R.id.conferenceWaitingRoomFragment, true) - ) -} - -internal fun ScheduledConferencesFragment.navigateToConferenceWaitingRoom( - address: String, - subject: String? -) { - val bundle = Bundle() - bundle.putString("Address", address) - bundle.putString("Subject", subject) - findMasterNavController().navigate( - R.id.action_global_conferenceWaitingRoomFragment, - bundle, - popupTo(R.id.conferenceWaitingRoomFragment, true) - ) -} - -internal fun ScheduledConferencesFragment.navigateToConferenceScheduling() { - findMasterNavController().navigate( - R.id.action_global_conferenceSchedulingFragment, - null, - popupTo(R.id.conferenceSchedulingFragment, true) - ) -} - -/* Chat related */ - -internal fun MasterChatRoomsFragment.navigateToChatRoom(args: Bundle) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_detailChatRoomFragment, - args, - popupTo(R.id.emptyChatFragment, false) - ) -} - -internal fun MasterChatRoomsFragment.navigateToChatRoomCreation( - createGroupChatRoom: Boolean = false, - slidingPane: SlidingPaneLayout -) { - val bundle = bundleOf("createGroup" to createGroupChatRoom) - val navHostFragment = - childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_chatRoomCreationFragment, - bundle, - popupTo(R.id.emptyChatFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() -} - -internal fun MasterChatRoomsFragment.clearDisplayedChatRoom() { - if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_emptyChatFragment, - null, - popupTo(R.id.emptyChatFragment, true) - ) - } -} - -internal fun DetailChatRoomFragment.navigateToContacts(sipUriToAdd: String) { - if (sipUriToAdd.isEmpty()) { - Log.e("[Navigation] SIP URI to add to contact is empty!") - return - } - - val deepLink = "linphone-android://contact/new/$sipUriToAdd" - findMasterNavController().navigate(Uri.parse(deepLink)) -} - -internal fun DetailChatRoomFragment.navigateToNativeContact(id: String) { - val deepLink = "linphone-android://contact/view/$id" - findMasterNavController().navigate(Uri.parse(deepLink)) -} - -internal fun DetailChatRoomFragment.navigateToFriend(address: String) { - val deepLink = "linphone-android://contact/view-friend/$address" - findMasterNavController().navigate(Uri.parse(deepLink)) -} - -internal fun DetailChatRoomFragment.navigateToImdn(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) { - findNavController().navigate( - R.id.action_detailChatRoomFragment_to_imdnFragment, - args, - popupTo() - ) - } -} - -internal fun DetailChatRoomFragment.navigateToDevices() { - if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) { - findNavController().navigate( - R.id.action_detailChatRoomFragment_to_devicesFragment, - null, - popupTo() - ) - } -} - -internal fun DetailChatRoomFragment.navigateToGroupInfo() { - if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) { - findNavController().navigate( - R.id.action_detailChatRoomFragment_to_groupInfoFragment, - null, - popupTo(R.id.groupInfoFragment, true) - ) - } -} - -internal fun DetailChatRoomFragment.navigateToEphemeralInfo() { - if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) { - findNavController().navigate( - R.id.action_detailChatRoomFragment_to_ephemeralFragment, - null, - popupTo() - ) - } -} - -internal fun DetailChatRoomFragment.navigateToTextFileViewer(secure: Boolean) { - val bundle = bundleOf("Secure" to secure) - findMasterNavController().navigate( - R.id.action_global_textViewerFragment, - bundle, - popupTo() - ) -} - -internal fun DetailChatRoomFragment.navigateToPdfFileViewer(secure: Boolean) { - val bundle = bundleOf("Secure" to secure) - findMasterNavController().navigate( - R.id.action_global_pdfViewerFragment, - bundle, - popupTo() - ) -} - -internal fun DetailChatRoomFragment.navigateToImageFileViewer(secure: Boolean) { - val bundle = bundleOf("Secure" to secure) - findMasterNavController().navigate( - R.id.action_global_imageViewerFragment, - bundle, - popupTo() - ) -} - -internal fun DetailChatRoomFragment.navigateToVideoFileViewer(secure: Boolean) { - val bundle = bundleOf("Secure" to secure) - findMasterNavController().navigate( - R.id.action_global_videoViewerFragment, - bundle, - popupTo() - ) -} - -internal fun DetailChatRoomFragment.navigateToAudioFileViewer(secure: Boolean) { - val bundle = bundleOf("Secure" to secure) - findMasterNavController().navigate( - R.id.action_global_audioViewerFragment, - bundle, - popupTo() - ) -} - -internal fun DetailChatRoomFragment.navigateToEmptyChatRoom() { - findNavController().navigate( - R.id.action_global_emptyChatFragment, - null, - popupTo(R.id.detailChatRoomFragment, true) - ) -} - -internal fun DetailChatRoomFragment.navigateToDialer(args: Bundle?) { - findMasterNavController().navigate( - R.id.action_global_dialerFragment, - args, - popupTo(R.id.dialerFragment, true) - ) -} - -internal fun DetailChatRoomFragment.navigateToConferenceScheduling() { - findMasterNavController().navigate( - R.id.action_global_conferenceSchedulingFragment, - null, - popupTo() - ) -} - -internal fun ChatRoomCreationFragment.navigateToGroupInfo() { - if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) { - findNavController().navigate( - R.id.action_chatRoomCreationFragment_to_groupInfoFragment, - null, - popupTo(R.id.groupInfoFragment, true) - ) - } -} - -internal fun ChatRoomCreationFragment.navigateToChatRoom(args: Bundle) { - if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) { - findNavController().navigate( - R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment, - args, - popupTo(R.id.emptyChatFragment, false) - ) - } -} - -internal fun GroupInfoFragment.navigateToChatRoomCreation(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.groupInfoFragment) { - findNavController().navigate( - R.id.action_groupInfoFragment_to_chatRoomCreationFragment, - args, - popupTo(R.id.chatRoomCreationFragment, true) - ) - } -} - -internal fun GroupInfoFragment.navigateToChatRoom(args: Bundle?, created: Boolean) { - if (findNavController().currentDestination?.id == R.id.groupInfoFragment) { - val popUpToFragmentId = if (created) { // To remove all creation fragments from back stack - R.id.chatRoomCreationFragment - } else { - R.id.detailChatRoomFragment - } - findNavController().navigate( - R.id.action_groupInfoFragment_to_detailChatRoomFragment, - args, - popupTo(popUpToFragmentId, true) - ) - } -} - -/* Contacts related */ - -internal fun MasterContactsFragment.navigateToContact() { - if (findNavController().currentDestination?.id == R.id.masterContactsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_detailContactFragment, - null, - popupTo(R.id.emptyContactFragment, false) - ) - } -} - -internal fun MasterContactsFragment.navigateToContactEditor( - sipUriToAdd: String? = null, - slidingPane: SlidingPaneLayout -) { - if (findNavController().currentDestination?.id == R.id.masterContactsFragment) { - val bundle = if (sipUriToAdd != null) bundleOf("SipUri" to sipUriToAdd) else Bundle() - val navHostFragment = - childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_contactEditorFragment, - bundle, - popupTo(R.id.emptyContactFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun MasterContactsFragment.clearDisplayedContact() { - if (findNavController().currentDestination?.id == R.id.masterContactsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_emptyContactFragment, - null, - popupTo(R.id.emptyContactFragment, true) - ) - } -} - -internal fun ContactEditorFragment.navigateToContact(id: String) { - if (findNavController().currentDestination?.id == R.id.contactEditorFragment) { - val bundle = Bundle() - bundle.putString("id", id) - findNavController().navigate( - R.id.action_contactEditorFragment_to_detailContactFragment, - bundle, - popupTo(R.id.contactEditorFragment, true) - ) - } -} - -internal fun DetailContactFragment.navigateToChatRoom(args: Bundle?) { - findMasterNavController().navigate( - R.id.action_global_masterChatRoomsFragment, - args, - popupTo(R.id.masterChatRoomsFragment, true) - ) -} - -internal fun DetailContactFragment.navigateToDialer(args: Bundle?) { - findMasterNavController().navigate( - R.id.action_global_dialerFragment, - args, - popupTo(R.id.dialerFragment, true) - ) -} - -internal fun DetailContactFragment.navigateToContactEditor() { - if (findNavController().currentDestination?.id == R.id.detailContactFragment) { - findNavController().navigate( - R.id.action_detailContactFragment_to_contactEditorFragment, - null, - popupTo(R.id.contactEditorFragment, true) - ) - } -} - -/* History related */ - -internal fun MasterCallLogsFragment.navigateToCallHistory(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_detailCallLogFragment, - null, - popupTo(R.id.emptyCallHistoryFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun MasterCallLogsFragment.navigateToConferenceCallHistory(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_detailConferenceCallLogFragment, - null, - popupTo(R.id.emptyCallHistoryFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun MasterCallLogsFragment.clearDisplayedCallHistory() { - if (findNavController().currentDestination?.id == R.id.masterCallLogsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_emptyFragment, - null, - popupTo(R.id.emptyCallHistoryFragment, true) - ) - } -} - -internal fun MasterCallLogsFragment.navigateToDialer(args: Bundle?) { - findNavController().navigate( - R.id.action_global_dialerFragment, - args, - popupTo(R.id.dialerFragment, true) - ) -} - -internal fun MasterCallLogsFragment.navigateToConferenceWaitingRoom( - address: String, - subject: String? -) { - val bundle = Bundle() - bundle.putString("Address", address) - bundle.putString("Subject", subject) - findMasterNavController().navigate( - R.id.action_global_conferenceWaitingRoomFragment, - bundle, - popupTo(R.id.conferenceWaitingRoomFragment, true) - ) -} - -internal fun DetailCallLogFragment.navigateToContacts(sipUriToAdd: String) { - if (sipUriToAdd.isEmpty()) { - Log.e("[Navigation] SIP URI to add to contact is empty!") - return - } - - val deepLink = "linphone-android://contact/new/$sipUriToAdd" - findMasterNavController().navigate(Uri.parse(deepLink)) -} - -internal fun DetailCallLogFragment.navigateToNativeContact(id: String) { - val deepLink = "linphone-android://contact/view/$id" - findMasterNavController().navigate(Uri.parse(deepLink)) -} - -internal fun DetailCallLogFragment.navigateToFriend(address: String) { - val deepLink = "linphone-android://contact/view-friend/$address" - findMasterNavController().navigate(Uri.parse(deepLink)) -} - -internal fun DetailCallLogFragment.navigateToChatRoom(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.detailCallLogFragment) { - findMasterNavController().navigate( - R.id.action_global_masterChatRoomsFragment, - args, - popupTo(R.id.masterChatRoomsFragment, true) - ) - } -} - -internal fun DetailCallLogFragment.navigateToDialer(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.detailCallLogFragment) { - findMasterNavController().navigate( - R.id.action_global_dialerFragment, - args, - popupTo(R.id.dialerFragment, true) - ) - } -} - -/* Settings related */ - -internal fun SettingsFragment.navigateToAccountSettings(identity: String) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val bundle = bundleOf("Identity" to identity) - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_accountSettingsFragment, - bundle, - popupTo(R.id.emptySettingsFragment, false) - ) - } -} - -internal fun SettingsFragment.navigateToTunnelSettings(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_tunnelSettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun SettingsFragment.navigateToAudioSettings(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_audioSettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun SettingsFragment.navigateToVideoSettings(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_videoSettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun SettingsFragment.navigateToCallSettings(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_callSettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun SettingsFragment.navigateToChatSettings(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_chatSettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun SettingsFragment.navigateToNetworkSettings(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_networkSettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun SettingsFragment.navigateToContactsSettings(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_contactsSettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun SettingsFragment.navigateToAdvancedSettings(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_advancedSettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun SettingsFragment.navigateToConferencesSettings(slidingPane: SlidingPaneLayout) { - if (findNavController().currentDestination?.id == R.id.settingsFragment) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - navHostFragment.navController.navigate( - R.id.action_global_conferencesSettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, false) - ) - if (!slidingPane.isOpen) slidingPane.openPane() - } -} - -internal fun AccountSettingsFragment.navigateToPhoneLinking(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.accountSettingsFragment) { - findNavController().navigate( - R.id.action_accountSettingsFragment_to_phoneAccountLinkingFragment, - args, - popupTo() - ) - } -} - -internal fun PhoneAccountLinkingFragment.navigateToPhoneAccountValidation(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) { - findNavController().navigate( - R.id.action_phoneAccountLinkingFragment_to_phoneAccountValidationFragment, - args, - popupTo() - ) - } -} - -internal fun navigateToEmptySetting(navController: NavController) { - navController.navigate( - R.id.action_global_emptySettingsFragment, - null, - popupTo(R.id.emptySettingsFragment, true) - ) -} - -internal fun ContactsSettingsFragment.navigateToLdapSettings(configIndex: Int) { - if (findNavController().currentDestination?.id == R.id.contactsSettingsFragment) { - val bundle = bundleOf("LdapConfigIndex" to configIndex) - findNavController().navigate( - R.id.action_contactsSettingsFragment_to_ldapSettingsFragment, - bundle, - popupTo() - ) - } -} - -/* Side menu related */ - -internal fun SideMenuFragment.navigateToAccountSettings(identity: String) { - val deepLink = "linphone-android://settings/$identity" - try { - findNavController().navigate(Uri.parse(deepLink)) - } catch (iae: IllegalArgumentException) { - Log.e("[Navigation] Failed to navigate to deeplink [$deepLink]") - } -} - -internal fun SideMenuFragment.navigateToSettings() { - findNavController().navigate( - R.id.action_global_settingsFragment, - null, - popupTo(R.id.settingsFragment, true) - ) -} - -internal fun SideMenuFragment.navigateToAbout() { - findNavController().navigate( - R.id.action_global_aboutFragment, - null, - popupTo(R.id.aboutFragment, true) - ) -} - -internal fun SideMenuFragment.navigateToRecordings() { - findNavController().navigate( - R.id.action_global_recordingsFragment, - null, - popupTo(R.id.recordingsFragment, true) - ) -} - -internal fun SideMenuFragment.navigateToScheduledConferences() { - findNavController().navigate( - R.id.action_global_scheduledConferencesFragment, - null, - popupTo(R.id.scheduledConferencesFragment, true) - ) -} - -/* Calls related */ - -internal fun CallActivity.navigateToActiveCall() { - if (findNavController(R.id.nav_host_fragment).currentDestination?.id != R.id.singleCallFragment) { - findNavController(R.id.nav_host_fragment).navigate( - R.id.action_global_singleCallFragment, - null, - popupTo(R.id.conferenceCallFragment, true) - ) - } -} - -internal fun CallActivity.navigateToConferenceCall() { - if (findNavController(R.id.nav_host_fragment).currentDestination?.id != R.id.conferenceCallFragment) { - findNavController(R.id.nav_host_fragment).navigate( - R.id.action_global_conferenceCallFragment, - null, - popupTo(R.id.singleCallFragment, true) - ) - } -} - -internal fun CallActivity.navigateToOutgoingCall() { - findNavController(R.id.nav_host_fragment).navigate( - R.id.action_global_outgoingCallFragment, - null, - popupTo(R.id.singleCallFragment, true) - ) -} - -internal fun CallActivity.navigateToIncomingCall(earlyMediaVideoEnabled: Boolean) { - val args = Bundle() - args.putBoolean("earlyMediaVideo", earlyMediaVideoEnabled) - findNavController(R.id.nav_host_fragment).navigate( - R.id.action_global_incomingCallFragment, - args, - popupTo(R.id.singleCallFragment, true) - ) -} - -internal fun OutgoingCallFragment.navigateToActiveCall() { - findNavController().navigate( - R.id.action_global_singleCallFragment, - null, - popupTo(R.id.outgoingCallFragment, true) - ) -} - -internal fun IncomingCallFragment.navigateToActiveCall() { - findNavController().navigate( - R.id.action_global_singleCallFragment, - null, - popupTo(R.id.incomingCallFragment, true) - ) -} - -internal fun SingleCallFragment.navigateToCallsList() { - if (findNavController().currentDestination?.id == R.id.singleCallFragment) { - findNavController().navigate( - R.id.action_singleCallFragment_to_callsListFragment, - null, - popupTo() - ) - } -} - -internal fun SingleCallFragment.navigateToConferenceParticipants() { - if (findNavController().currentDestination?.id == R.id.singleCallFragment) { - findNavController().navigate( - R.id.action_singleCallFragment_to_conferenceParticipantsFragment, - null, - popupTo() - ) - } -} - -internal fun SingleCallFragment.navigateToConferenceLayout() { - if (findNavController().currentDestination?.id == R.id.singleCallFragment) { - findNavController().navigate( - R.id.action_singleCallFragment_to_conferenceLayoutFragment, - null, - popupTo() - ) - } -} - -internal fun SingleCallFragment.navigateToIncomingCall() { - if (findNavController().currentDestination?.id == R.id.singleCallFragment) { - findNavController().navigate( - R.id.action_global_incomingCallFragment, - null, - popupTo(R.id.singleCallFragment, true) - ) - } -} - -internal fun SingleCallFragment.navigateToOutgoingCall() { - if (findNavController().currentDestination?.id == R.id.singleCallFragment) { - findNavController().navigate( - R.id.action_global_outgoingCallFragment, - null, - popupTo(R.id.singleCallFragment, true) - ) - } -} - -internal fun ConferenceCallFragment.navigateToCallsList() { - if (findNavController().currentDestination?.id == R.id.conferenceCallFragment) { - findNavController().navigate( - R.id.action_conferenceCallFragment_to_callsListFragment, - null, - popupTo() - ) - } -} - -internal fun ConferenceCallFragment.navigateToConferenceParticipants() { - if (findNavController().currentDestination?.id == R.id.conferenceCallFragment) { - findNavController().navigate( - R.id.action_conferenceCallFragment_to_conferenceParticipantsFragment, - null, - popupTo() - ) - } -} - -internal fun ConferenceCallFragment.navigateToConferenceLayout() { - if (findNavController().currentDestination?.id == R.id.conferenceCallFragment) { - findNavController().navigate( - R.id.action_conferenceCallFragment_to_conferenceLayoutFragment, - null, - popupTo() - ) - } -} - -internal fun ConferenceCallFragment.refreshConferenceFragment() { - if (findNavController().currentDestination?.id == R.id.conferenceCallFragment) { - findNavController().navigate( - R.id.action_global_conferenceCallFragment, - null, - popupTo(R.id.conferenceCallFragment, true) - ) - } -} - -internal fun ConferenceParticipantsFragment.navigateToAddParticipants() { - if (findNavController().currentDestination?.id == R.id.conferenceParticipantsFragment) { - findNavController().navigate( - R.id.action_conferenceParticipantsFragment_to_conferenceAddParticipantsFragment, - null, - popupTo(R.id.conferenceAddParticipantsFragment, true) - ) - } -} - -/* Assistant related */ - -internal fun WelcomeFragment.navigateToEmailAccountCreation() { - if (findNavController().currentDestination?.id == R.id.welcomeFragment) { - findNavController().navigate( - R.id.action_welcomeFragment_to_emailAccountCreationFragment, - null, - popupTo() - ) - } -} - -internal fun WelcomeFragment.navigateToPhoneAccountCreation() { - if (findNavController().currentDestination?.id == R.id.welcomeFragment) { - findNavController().navigate( - R.id.action_welcomeFragment_to_phoneAccountCreationFragment, - null, - popupTo() - ) - } -} - -internal fun WelcomeFragment.navigateToNoPushWarning() { - if (findNavController().currentDestination?.id == R.id.welcomeFragment) { - findNavController().navigate( - R.id.action_welcomeFragment_to_noPushWarningFragment, - null, - popupTo() - ) - } -} - -internal fun WelcomeFragment.navigateToAccountLogin() { - if (findNavController().currentDestination?.id == R.id.welcomeFragment) { - findNavController().navigate( - R.id.action_welcomeFragment_to_accountLoginFragment, - null, - popupTo() - ) - } -} - -internal fun WelcomeFragment.navigateToGenericLoginWarning() { - if (findNavController().currentDestination?.id == R.id.welcomeFragment) { - findNavController().navigate( - R.id.action_welcomeFragment_to_genericAccountWarningFragment, - null, - popupTo() - ) - } -} - -internal fun WelcomeFragment.navigateToRemoteProvisioning() { - if (findNavController().currentDestination?.id == R.id.welcomeFragment) { - findNavController().navigate( - R.id.action_welcomeFragment_to_remoteProvisioningFragment, - null, - popupTo() - ) - } -} - -internal fun AccountLoginFragment.navigateToEchoCancellerCalibration() { - if (findNavController().currentDestination?.id == R.id.accountLoginFragment) { - findNavController().navigate( - R.id.action_accountLoginFragment_to_echoCancellerCalibrationFragment, - null, - popupTo() - ) - } -} - -internal fun AccountLoginFragment.navigateToPhoneAccountValidation(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.accountLoginFragment) { - findNavController().navigate( - R.id.action_accountLoginFragment_to_phoneAccountValidationFragment, - args, - popupTo() - ) - } -} - -internal fun GenericAccountWarningFragment.navigateToGenericLogin() { - if (findNavController().currentDestination?.id == R.id.genericAccountWarningFragment) { - findNavController().navigate( - R.id.action_genericAccountWarningFragment_to_genericAccountLoginFragment, - null, - popupTo(R.id.welcomeFragment, popUpInclusive = false) - ) - } -} - -internal fun GenericAccountLoginFragment.navigateToEchoCancellerCalibration() { - if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) { - findNavController().navigate( - R.id.action_genericAccountLoginFragment_to_echoCancellerCalibrationFragment, - null, - popupTo() - ) - } -} - -internal fun RemoteProvisioningFragment.navigateToQrCode() { - if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) { - findNavController().navigate( - R.id.action_remoteProvisioningFragment_to_qrCodeFragment, - null, - popupTo() - ) - } -} - -internal fun RemoteProvisioningFragment.navigateToEchoCancellerCalibration() { - if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) { - findNavController().navigate( - R.id.action_remoteProvisioningFragment_to_echoCancellerCalibrationFragment, - null, - popupTo() - ) - } -} - -internal fun EmailAccountCreationFragment.navigateToEmailAccountValidation() { - if (findNavController().currentDestination?.id == R.id.emailAccountCreationFragment) { - findNavController().navigate( - R.id.action_emailAccountCreationFragment_to_emailAccountValidationFragment, - null, - popupTo() - ) - } -} - -internal fun EmailAccountValidationFragment.navigateToAccountLinking(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.emailAccountValidationFragment) { - findNavController().navigate( - R.id.action_emailAccountValidationFragment_to_phoneAccountLinkingFragment, - args, - popupTo() - ) - } -} - -internal fun PhoneAccountCreationFragment.navigateToPhoneAccountValidation(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.phoneAccountCreationFragment) { - findNavController().navigate( - R.id.action_phoneAccountCreationFragment_to_phoneAccountValidationFragment, - args, - popupTo() - ) - } -} - -internal fun PhoneAccountValidationFragment.navigateToAccountSettings(args: Bundle?) { - if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) { - findNavController().navigate( - R.id.action_phoneAccountValidationFragment_to_accountSettingsFragment, - args, - popupTo(R.id.accountSettingsFragment, true) - ) - } -} - -internal fun PhoneAccountValidationFragment.navigateToEchoCancellerCalibration() { - if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) { - findNavController().navigate( - R.id.action_phoneAccountValidationFragment_to_echoCancellerCalibrationFragment, - null, - popupTo() - ) - } -} - -internal fun PhoneAccountLinkingFragment.navigateToEchoCancellerCalibration() { - if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) { - findNavController().navigate( - R.id.action_phoneAccountLinkingFragment_to_echoCancellerCalibrationFragment, - null, - popupTo() - ) - } -} diff --git a/app/src/main/java/org/linphone/activities/ProximitySensorActivity.kt b/app/src/main/java/org/linphone/activities/ProximitySensorActivity.kt deleted file mode 100644 index 605af6df8..000000000 --- a/app/src/main/java/org/linphone/activities/ProximitySensorActivity.kt +++ /dev/null @@ -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 . - */ -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 - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/SnackBarActivity.kt b/app/src/main/java/org/linphone/activities/SnackBarActivity.kt deleted file mode 100644 index c9995d68c..000000000 --- a/app/src/main/java/org/linphone/activities/SnackBarActivity.kt +++ /dev/null @@ -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 . - */ -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) -} diff --git a/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt b/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt deleted file mode 100644 index 1b1b8321e..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt +++ /dev/null @@ -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 . - */ -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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/adapters/CountryPickerAdapter.kt b/app/src/main/java/org/linphone/activities/assistant/adapters/CountryPickerAdapter.kt deleted file mode 100644 index 47220e0a7..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/adapters/CountryPickerAdapter.kt +++ /dev/null @@ -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 . - */ -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 - - 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(R.id.country_name) - name.text = dialPlan.country - - val dialCode = view.findViewById(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() - 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 - notifyDataSetChanged() - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt deleted file mode 100644 index 01c33748a..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt +++ /dev/null @@ -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 . - */ - -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 : GenericFragment() { - 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, - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt deleted file mode 100644 index e9a35b94b..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt deleted file mode 100644 index 2e3d4dc72..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt +++ /dev/null @@ -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 . - */ -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) - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt deleted file mode 100644 index 9c48cb0d0..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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, - 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() - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt deleted file mode 100644 index 027636460..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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) - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt deleted file mode 100644 index 98b5548bd..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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) - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt deleted file mode 100644 index a93eea348..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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) - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountWarningFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountWarningFragment.kt deleted file mode 100644 index 8c41c0653..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountWarningFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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() - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/NoPushWarningFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/NoPushWarningFragment.kt deleted file mode 100644 index 1671474bd..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/NoPushWarningFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt deleted file mode 100644 index 4f285e8b5..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt deleted file mode 100644 index d8fd87e22..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt deleted file mode 100644 index edcf18210..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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) - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt deleted file mode 100644 index 0fe20d93e..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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, - 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() - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt deleted file mode 100644 index 5889ba271..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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 - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/TopBarFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/TopBarFragment.kt deleted file mode 100644 index e1e8ef074..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/TopBarFragment.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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() { - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt b/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt deleted file mode 100644 index 79c54f64d..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt deleted file mode 100644 index 605b0c5ce..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt +++ /dev/null @@ -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 . - */ - -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() - val prefixError = MutableLiveData() - - val phoneNumber = MutableLiveData() - val phoneNumberError = MutableLiveData() - - val countryName = MutableLiveData() - - 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 - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPushTokenViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPushTokenViewModel.kt deleted file mode 100644 index 363475e63..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPushTokenViewModel.kt +++ /dev/null @@ -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 . - */ -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() - } - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt deleted file mode 100644 index 579a76eb7..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return AccountLoginViewModel(accountCreator) as T - } -} - -class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) { - val loginWithUsernamePassword = MutableLiveData() - - val username = MutableLiveData() - val usernameError = MutableLiveData() - - val password = MutableLiveData() - val passwordError = MutableLiveData() - - val loginEnabled = MediatorLiveData() - - val waitForServerAnswer = MutableLiveData() - - val displayName = MutableLiveData() - - val forceLoginUsingUsernameAndPassword = MutableLiveData() - - val leaveAssistantEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val invalidCredentialsEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val goToSmsValidationEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val onErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt deleted file mode 100644 index 09d01c069..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt +++ /dev/null @@ -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 . - */ - -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>() - - 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) - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt deleted file mode 100644 index dea25074d..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return EmailAccountCreationViewModel(accountCreator) as T - } -} - -class EmailAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPushTokenViewModel( - accountCreator -) { - val username = MutableLiveData() - val usernameError = MutableLiveData() - - val email = MutableLiveData() - val emailError = MutableLiveData() - - val password = MutableLiveData() - val passwordError = MutableLiveData() - - val passwordConfirmation = MutableLiveData() - val passwordConfirmationError = MutableLiveData() - - val displayName = MutableLiveData() - - val createEnabled: MediatorLiveData = MediatorLiveData() - - val waitForServerAnswer = MutableLiveData() - - val goToEmailValidationEvent = MutableLiveData>() - - val onErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt deleted file mode 100644 index bc72fbc90..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return EmailAccountValidationViewModel(accountCreator) as T - } -} - -class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() { - val email = MutableLiveData() - - val waitForServerAnswer = MutableLiveData() - - val leaveAssistantEvent = MutableLiveData>() - - val onErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt deleted file mode 100644 index 5e0e50dfe..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return GenericLoginViewModel(accountCreator) as T - } -} - -class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewModel() { - val username = MutableLiveData() - - val password = MutableLiveData() - - val domain = MutableLiveData() - - val displayName = MutableLiveData() - - val transport = MutableLiveData() - - val loginEnabled: MediatorLiveData = MediatorLiveData() - - val waitForServerAnswer = MutableLiveData() - - val leaveAssistantEvent = MutableLiveData>() - - val invalidCredentialsEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val onErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt deleted file mode 100644 index 03b6face3..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt +++ /dev/null @@ -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 . - */ - -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 create(modelClass: Class): T { - return PhoneAccountCreationViewModel(accountCreator) as T - } -} - -class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel( - accountCreator -) { - val username = MutableLiveData() - val useUsername = MutableLiveData() - val usernameError = MutableLiveData() - - val displayName = MutableLiveData() - - val createEnabled: MediatorLiveData = MediatorLiveData() - - val waitForServerAnswer = MutableLiveData() - - val goToSmsValidationEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val onErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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() - ) - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt deleted file mode 100644 index 78c11c04e..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt +++ /dev/null @@ -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 . - */ - -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 create(modelClass: Class): T { - return PhoneAccountLinkingViewModel(accountCreator) as T - } -} - -class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel( - accountCreator -) { - val username = MutableLiveData() - - val allowSkip = MutableLiveData() - - val linkEnabled: MediatorLiveData = MediatorLiveData() - - val waitForServerAnswer = MutableLiveData() - - val leaveAssistantEvent = MutableLiveData>() - - val goToSmsValidationEvent = MutableLiveData>() - - val onErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountValidationViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountValidationViewModel.kt deleted file mode 100644 index cf1f3f787..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountValidationViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return PhoneAccountValidationViewModel(accountCreator) as T - } -} - -class PhoneAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() { - val phoneNumber = MutableLiveData() - - val code = MutableLiveData() - - val isLogin = MutableLiveData() - - val isCreation = MutableLiveData() - - val isLinking = MutableLiveData() - - val waitForServerAnswer = MutableLiveData() - - val leaveAssistantEvent = MutableLiveData>() - - val onErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt deleted file mode 100644 index c0f988d0f..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt +++ /dev/null @@ -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 . - */ -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>() - - val showSwitchCamera = MutableLiveData() - - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt deleted file mode 100644 index 2f8d75b90..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt +++ /dev/null @@ -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 . - */ -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() - val urlError = MutableLiveData() - - val fetchEnabled: MediatorLiveData = MediatorLiveData() - val fetchInProgress = MutableLiveData() - val fetchSuccessfulEvent = MutableLiveData>() - - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/SharedAssistantViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/SharedAssistantViewModel.kt deleted file mode 100644 index d97bdf1b7..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/SharedAssistantViewModel.kt +++ /dev/null @@ -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 . - */ -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() - - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/assistant/viewmodels/WelcomeViewModel.kt b/app/src/main/java/org/linphone/activities/assistant/viewmodels/WelcomeViewModel.kt deleted file mode 100644 index d31a2ba89..000000000 --- a/app/src/main/java/org/linphone/activities/assistant/viewmodels/WelcomeViewModel.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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() - - init { - termsAndPrivacyAccepted.value = corePreferences.readAndAgreeTermsAndPrivacy - } -} diff --git a/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt b/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt deleted file mode 100644 index ddcdefa5b..000000000 --- a/app/src/main/java/org/linphone/activities/chat_bubble/ChatBubbleActivity.kt +++ /dev/null @@ -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 . - */ -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) { - 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) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/MainActivity.kt b/app/src/main/java/org/linphone/activities/main/MainActivity.kt deleted file mode 100644 index 78798c4c0..000000000 --- a/app/src/main/java/org/linphone/activities/main/MainActivity.kt +++ /dev/null @@ -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 . - */ -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> by lazy { - MutableLiveData>() - } - 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() - - 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(Intent.EXTRA_STREAM) as? Uri)?.let { - val list = arrayListOf() - 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(Intent.EXTRA_STREAM)?.let { - val list = arrayListOf() - coroutineScope { - val deferred = arrayListOf>() - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt b/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt deleted file mode 100644 index e81703192..000000000 --- a/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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") - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/about/AboutViewModel.kt b/app/src/main/java/org/linphone/activities/main/about/AboutViewModel.kt deleted file mode 100644 index 2961af522..000000000 --- a/app/src/main/java/org/linphone/activities/main/about/AboutViewModel.kt +++ /dev/null @@ -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 . - */ -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 -} diff --git a/app/src/main/java/org/linphone/activities/main/adapters/SelectionListAdapter.kt b/app/src/main/java/org/linphone/activities/main/adapters/SelectionListAdapter.kt deleted file mode 100644 index e02070b29..000000000 --- a/app/src/main/java/org/linphone/activities/main/adapters/SelectionListAdapter.kt +++ /dev/null @@ -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 . - */ -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( - selectionVM: ListTopBarViewModel, - diff: DiffUtil.ItemCallback -) : - ListAdapter(diff) { - - private var _selectionViewModel: ListTopBarViewModel? = selectionVM - protected val selectionViewModel get() = _selectionViewModel!! - - override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { - super.onDetachedFromRecyclerView(recyclerView) - _selectionViewModel = null - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt b/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt deleted file mode 100644 index 795be99ed..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.main.chat - -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView - -internal abstract class ChatScrollListener(private val mLayoutManager: LinearLayoutManager) : - RecyclerView.OnScrollListener() { - // The total number of items in the data set after the last load - private var previousTotalItemCount = 0 - - // True if we are still waiting for the last set of data to load. - private var loading = true - - private var userHasScrolledUp: Boolean = false - - // This happens many times a second during a scroll, so be wary of the code you place here. - // We are given a few useful parameters to help us work out if we need to load some more data, - // but first we check if we are waiting for the previous load to finish. - override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { - val totalItemCount = mLayoutManager.itemCount - val firstVisibleItemPosition: Int = mLayoutManager.findFirstVisibleItemPosition() - val lastVisibleItemPosition: Int = mLayoutManager.findLastVisibleItemPosition() - - // If the total item count is zero and the previous isn't, assume the - // list is invalidated and should be reset back to initial state - if (totalItemCount < previousTotalItemCount) { - previousTotalItemCount = totalItemCount - if (totalItemCount == 0) { - loading = true - } - } - - // If it’s still loading, we check to see if the data set count has - // changed, if so we conclude it has finished loading and update the current page - // number and total item count. - if (loading && totalItemCount > previousTotalItemCount) { - loading = false - previousTotalItemCount = totalItemCount - } - - userHasScrolledUp = lastVisibleItemPosition != totalItemCount - 1 - if (userHasScrolledUp) { - onScrolledUp() - } else { - onScrolledToEnd() - } - - // If it isn’t currently loading, we check to see if we have breached - // the mVisibleThreshold and need to reload more data. - // If we do need to reload some more data, we execute onLoadMore to fetch the data. - // threshold should reflect how many total columns there are too - if (!loading && - firstVisibleItemPosition < mVisibleThreshold && - firstVisibleItemPosition >= 0 && - lastVisibleItemPosition < totalItemCount - mVisibleThreshold - ) { - onLoadMore(totalItemCount) - loading = true - } - } - - // Defines the process for actually loading more data based on page - protected abstract fun onLoadMore(totalItemsCount: Int) - - // Called when user has started to scroll up, opposed to onScrolledToEnd() - protected abstract fun onScrolledUp() - - // Called when user has scrolled and reached the end of the items - protected abstract fun onScrolledToEnd() - - companion object { - // The minimum amount of items to have below your current scroll position - // before loading more. - private const val mVisibleThreshold = 5 - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/GroupChatRoomMember.kt b/app/src/main/java/org/linphone/activities/main/chat/GroupChatRoomMember.kt deleted file mode 100644 index 537b8017c..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/GroupChatRoomMember.kt +++ /dev/null @@ -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 . - */ -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 -) diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt deleted file mode 100644 index d922ccd93..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt +++ /dev/null @@ -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 . - */ -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( - selectionVM, - ChatMessageDiffCallback() -), - HeaderAdapter { - companion object { - const val MAX_TIME_TO_GROUP_MESSAGES = 60 // 1 minute - } - - val resendMessageEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val deleteMessageEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val forwardMessageEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val replyMessageEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val showImdnForMessageEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val addSipUriToContactEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val openContentEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val urlClickEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val sipUriClickedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val callConferenceEvent: MutableLiveData>> by lazy { - MutableLiveData>>() - } - - val scrollToChatMessageEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val showReactionsListEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val errorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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, - currentList: MutableList - ) { - 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() { - 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 - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt deleted file mode 100644 index 12e6c7157..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt +++ /dev/null @@ -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 . - */ -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(selectionVM, ChatRoomDiffCallback()) { - val selectedChatRoomEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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() { - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/GroupInfoParticipantsAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/GroupInfoParticipantsAdapter.kt deleted file mode 100644 index b9d13bc60..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/adapters/GroupInfoParticipantsAdapter.kt +++ /dev/null @@ -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 . - */ -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(ParticipantDiffCallback()) { - private var showAdmin: Boolean = false - - val participantRemovedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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() { - override fun areItemsTheSame( - oldItem: GroupInfoParticipantData, - newItem: GroupInfoParticipantData - ): Boolean { - return oldItem.sipUri == newItem.sipUri - } - - override fun areContentsTheSame( - oldItem: GroupInfoParticipantData, - newItem: GroupInfoParticipantData - ): Boolean { - return false - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ImdnAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ImdnAdapter.kt deleted file mode 100644 index 3226780ea..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ImdnAdapter.kt +++ /dev/null @@ -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 . - */ -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(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() { - override fun areItemsTheSame( - oldItem: ImdnParticipantData, - newItem: ImdnParticipantData - ): Boolean { - return oldItem.sipUri == newItem.sipUri - } - - override fun areContentsTheSame( - oldItem: ImdnParticipantData, - newItem: ImdnParticipantData - ): Boolean { - return false - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageAttachmentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageAttachmentData.kt deleted file mode 100644 index fc66b68a5..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageAttachmentData.kt +++ /dev/null @@ -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 . - */ -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) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt deleted file mode 100644 index 451a89eb7..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt +++ /dev/null @@ -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 . - */ -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() - val isVideo = MutableLiveData() - val isAudio = MutableLiveData() - val isPdf = MutableLiveData() - val isGenericFile = MutableLiveData() - val isVoiceRecording = MutableLiveData() - val isConferenceSchedule = MutableLiveData() - val isConferenceUpdated = MutableLiveData() - val isConferenceCancelled = MutableLiveData() - val isBroadcast = MutableLiveData() - val isSpeaker = MutableLiveData() - - val fileName = MutableLiveData() - val filePath = MutableLiveData() - - val downloadable = MutableLiveData() - val fileTransferProgress = MutableLiveData() - val fileTransferProgressInt = MutableLiveData() - val downloadLabel = MutableLiveData() - - val voiceRecordDuration = MutableLiveData() - val formattedDuration = MutableLiveData() - val voiceRecordPlayingPosition = MutableLiveData() - val isVoiceRecordPlaying = MutableLiveData() - - val conferenceSubject = MutableLiveData() - val conferenceDescription = MutableLiveData() - val conferenceParticipantCount = MutableLiveData() - val conferenceDate = MutableLiveData() - val conferenceTime = MutableLiveData() - val conferenceDuration = MutableLiveData() - val showDuration = MutableLiveData() - - 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 { - "" - } - } 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) -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt deleted file mode 100644 index f4d27b5dc..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt +++ /dev/null @@ -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 . - */ -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() - - val showImdn = MutableLiveData() - - val imdnIcon = MutableLiveData() - - val backgroundRes = MutableLiveData() - - val hideAvatar = MutableLiveData() - - val hideTime = MutableLiveData() - - val contents = MutableLiveData>() - - val time = MutableLiveData() - - val ephemeralLifetime = MutableLiveData() - - val text = MutableLiveData() - - val isTextEmoji = MutableLiveData() - - val replyData = MutableLiveData() - - val isDisplayed = MutableLiveData() - - val isOutgoing = chatMessage.isOutgoing - - val contactNewlyFoundEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val reactions = MutableLiveData>() - - 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() - - 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( - "(?:)?" - ), - 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() - 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) - ) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionData.kt deleted file mode 100644 index c73b423d8..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionData.kt +++ /dev/null @@ -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 . - */ -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() - - init { - reaction.value = chatMessageReaction.body - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionsListData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionsListData.kt deleted file mode 100644 index 820195087..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionsListData.kt +++ /dev/null @@ -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 . - */ -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>() - - val filteredReactions = MutableLiveData>() - - val reactionsMap = HashMap() - - 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() - 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() - 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) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomData.kt deleted file mode 100644 index 233ae0985..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatRoomData.kt +++ /dev/null @@ -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 . - */ -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 = MutableLiveData() - override val displayName: MutableLiveData = MutableLiveData() - override val securityLevel: MutableLiveData = MutableLiveData() - override val showGroupChatAvatar: Boolean - get() = !oneToOneChatRoom - override val presenceStatus: MutableLiveData = MutableLiveData() - override val coroutineScope: CoroutineScope = coreContext.coroutineScope - - val id = LinphoneUtils.getChatRoomId(chatRoom) - - val unreadMessagesCount = MutableLiveData() - - val subject = MutableLiveData() - - val securityLevelIcon = MutableLiveData() - - val securityLevelContentDescription = MutableLiveData() - - val ephemeralEnabled = MutableLiveData() - - val lastUpdate = MutableLiveData() - - val lastMessageText = MutableLiveData() - - val showLastMessageImdnIcon = MutableLiveData() - - val lastMessageImdnIcon = MutableLiveData() - - val notificationsMuted = MutableLiveData() - - 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> by lazy { - MutableLiveData>() - } - - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt deleted file mode 100644 index e7216c946..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListChildData.kt +++ /dev/null @@ -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 . - */ -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) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt deleted file mode 100644 index c8620c03b..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/DevicesListGroupData.kt +++ /dev/null @@ -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 . - */ -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() - - val devices = MutableLiveData>() - - init { - securityLevel.value = participant.securityLevel - isExpanded.value = false - - val list = arrayListOf() - 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) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/EphemeralDurationData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/EphemeralDurationData.kt deleted file mode 100644 index ddf6b1d68..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/EphemeralDurationData.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -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) -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/EventData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/EventData.kt deleted file mode 100644 index 4c9f08ec8..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/EventData.kt +++ /dev/null @@ -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 . - */ -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() - - 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" - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/EventLogData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/EventLogData.kt deleted file mode 100644 index a20c19a94..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/EventLogData.kt +++ /dev/null @@ -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 . - */ -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() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/GroupInfoParticipantData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/GroupInfoParticipantData.kt deleted file mode 100644 index 977fca139..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/GroupInfoParticipantData.kt +++ /dev/null @@ -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 . - */ -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() - - val showAdminControls = MutableLiveData() - - // A participant not yet added to a group can't be set admin at the same time it's added - val canBeSetAdmin = MutableLiveData() - - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ImdnParticipantData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ImdnParticipantData.kt deleted file mode 100644 index 4d93970c3..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ImdnParticipantData.kt +++ /dev/null @@ -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 . - */ -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) -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatMessageReactionsListDialogFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatMessageReactionsListDialogFragment.kt deleted file mode 100644 index 099981e84..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatMessageReactionsListDialogFragment.kt +++ /dev/null @@ -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 . - */ -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() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt deleted file mode 100644 index 59c45515b..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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, - 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 - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt deleted file mode 100644 index 8b105a700..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt +++ /dev/null @@ -1,1369 +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 . - */ -package org.linphone.activities.main.chat.fragments - -import android.app.Activity -import android.app.Dialog -import android.content.* -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Bundle -import android.os.Parcelable -import android.provider.MediaStore -import android.view.* -import android.view.ViewTreeObserver.OnGlobalLayoutListener -import android.webkit.MimeTypeMap -import android.widget.PopupWindow -import androidx.core.content.ContextCompat -import androidx.core.content.FileProvider -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import java.io.File -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.main.MainActivity -import org.linphone.activities.main.chat.ChatScrollListener -import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter -import org.linphone.activities.main.chat.data.ChatMessageData -import org.linphone.activities.main.chat.data.EventLogData -import org.linphone.activities.main.chat.viewmodels.* -import org.linphone.activities.main.chat.views.RichEditTextSendListener -import org.linphone.activities.main.fragments.MasterFragment -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.compatibility.Compatibility -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.databinding.ChatRoomDetailFragmentBinding -import org.linphone.databinding.ChatRoomMenuBindingImpl -import org.linphone.utils.* -import org.linphone.utils.Event - -class DetailChatRoomFragment : MasterFragment() { - private lateinit var viewModel: ChatRoomViewModel - private lateinit var chatSendingViewModel: ChatMessageSendingViewModel - private lateinit var listViewModel: ChatMessagesListViewModel - - private val observer = object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - adapter.notifyItemChanged(positionStart - 1) // For grouping purposes - - if (positionStart == 0 && adapter.itemCount == itemCount) { - // First time we fill the list with messages - Log.i("[Chat Room] History first $itemCount messages loaded") - } else { - // Scroll to newly added messages automatically only if user hasn't initiated a scroll up in the messages history - if (viewModel.isUserScrollingUp.value == false) { - scrollToFirstUnreadMessageOrBottom(false) - } else { - Log.d( - "[Chat Room] User has scrolled up manually in the messages history, don't scroll to the newly added message at the bottom & don't mark the chat room as read" - ) - } - } - } - } - - private val globalLayoutLayout = object : OnGlobalLayoutListener { - override fun onGlobalLayout() { - if (isBindingAvailable()) { - binding.chatMessagesList - .viewTreeObserver - .removeOnGlobalLayoutListener(this) - - if (::chatScrollListener.isInitialized) { - binding.chatMessagesList.addOnScrollListener(chatScrollListener) - } - - if (viewModel.chatRoom.unreadMessagesCount > 0) { - Log.i("[Chat Room] Messages have been displayed, scrolling to first unread") - val notAllMessagesDisplayed = scrollToFirstUnreadMessageOrBottom(false) - if (notAllMessagesDisplayed) { - Log.w( - "[Chat Room] More unread messages than the screen can display, do not mark chat room as read now, wait for user to scroll to bottom" - ) - } else { - // Consider user as scrolled to the end when marking chat room as read - viewModel.isUserScrollingUp.value = false - Log.i("[Chat Room] Marking chat room as read") - viewModel.chatRoom.markAsRead() - } - } - } else { - Log.e("[Chat Room] Binding not available in onGlobalLayout callback!") - } - } - } - - private val keyboardVisibilityListener = object : AppUtils.KeyboardVisibilityListener { - override fun onKeyboardVisibilityChanged(visible: Boolean) { - if (visible && chatSendingViewModel.isEmojiPickerOpen.value == true) { - Log.d( - "[Chat Room] Emoji picker is opened, closing it because keyboard is now visible" - ) - chatSendingViewModel.isEmojiPickerOpen.value = false - } - } - } - - private lateinit var chatScrollListener: ChatScrollListener - - override fun getLayoutId(): Int { - return R.layout.chat_room_detail_fragment - } - - override fun onDestroyView() { - binding.chatMessagesList.adapter = null - - super.onDestroyView() - } - - override fun onSaveInstanceState(outState: Bundle) { - if (isSharedViewModelInitialized()) { - val chatRoom = sharedViewModel.selectedChatRoom.value - if (chatRoom != null) { - outState.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly()) - outState.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly()) - Log.i( - "[Chat Room] Saving current chat room local & remote addresses in save instance state" - ) - } - } else { - Log.w( - "[Chat Room] Can't save instance state, sharedViewModel hasn't been initialized yet" - ) - } - super.onSaveInstanceState(outState) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - postponeEnterTransition() - - binding.lifecycleOwner = viewLifecycleOwner - - binding.sharedMainViewModel = sharedViewModel - - useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false - - val localSipUri = arguments?.getString("LocalSipUri") ?: savedInstanceState?.getString( - "LocalSipUri" - ) - val remoteSipUri = arguments?.getString("RemoteSipUri") ?: savedInstanceState?.getString( - "RemoteSipUri" - ) - - val textToShare = arguments?.getString("TextToShare") - val filesToShare = arguments?.getStringArrayList("FilesToShare") - - if (remoteSipUri != null && arguments?.getString("RemoteSipUri") == null) { - Log.w("[Chat Room] Chat room will be restored from saved instance state") - } - arguments?.clear() - if (localSipUri != null && remoteSipUri != null) { - Log.i( - "[Chat Room] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments or saved instance state" - ) - - val localAddress = Factory.instance().createAddress(localSipUri) - val remoteSipAddress = Factory.instance().createAddress(remoteSipUri) - sharedViewModel.selectedChatRoom.value = coreContext.core.searchChatRoom( - null, - localAddress, - remoteSipAddress, - arrayOfNulls( - 0 - ) - ) - } - - val chatRoom = sharedViewModel.selectedChatRoom.value - if (chatRoom == null) { - Log.e("[Chat Room] Chat room is null, aborting!") - goBack() - return - } - - Compatibility.setLocusIdInContentCaptureSession(binding.root, chatRoom) - - isSecure = chatRoom.currentParams.isEncryptionEnabled - - viewModel = ViewModelProvider( - this, - ChatRoomViewModelFactory(chatRoom) - )[ChatRoomViewModel::class.java] - - binding.viewModel = viewModel - - chatSendingViewModel = ViewModelProvider( - this, - ChatMessageSendingViewModelFactory(chatRoom) - )[ChatMessageSendingViewModel::class.java] - binding.chatSendingViewModel = chatSendingViewModel - - listViewModel = ViewModelProvider( - this, - ChatMessagesListViewModelFactory(chatRoom) - )[ChatMessagesListViewModel::class.java] - - _adapter = ChatMessagesListAdapter(listSelectionViewModel, viewLifecycleOwner) - // SubmitList is done on a background thread - // We need this adapter data observer to know when to scroll - binding.chatMessagesList.adapter = adapter - - val layoutManager = LinearLayoutManager(requireContext()) - layoutManager.stackFromEnd = true - binding.chatMessagesList.layoutManager = layoutManager - - // Displays unread messages header - val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) - binding.chatMessagesList.addItemDecoration(headerItemDecoration) - - // Swipe action - val swipeConfiguration = RecyclerViewSwipeConfiguration() - // Reply action can only be done on a ChatMessageEventLog - swipeConfiguration.leftToRightAction = RecyclerViewSwipeConfiguration.Action( - text = requireContext().getString(R.string.chat_message_context_menu_reply), - backgroundColor = ContextCompat.getColor(requireContext(), R.color.light_grey_color), - preventFor = ChatMessagesListAdapter.EventViewHolder::class.java - ) - // Delete action can be done on any EventLog - swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action( - text = requireContext().getString(R.string.chat_message_context_menu_delete), - backgroundColor = ContextCompat.getColor(requireContext(), R.color.red_color) - ) - val swipeListener = object : RecyclerViewSwipeListener { - override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) { - val index = viewHolder.bindingAdapterPosition - if (index < 0 || index >= adapter.currentList.size) { - Log.e("[Chat Room] Index is out of bound, can't reply to chat message") - } else { - adapter.notifyItemChanged(index) - - val chatMessageEventLog = adapter.currentList[index] - val chatMessage = chatMessageEventLog.eventLog.chatMessage - if (chatMessage != null) { - replyToChatMessage(chatMessage) - } - } - } - - override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) { - val index = viewHolder.bindingAdapterPosition - if (index < 0 || index >= adapter.currentList.size) { - Log.e("[Chat Room] Index is out of bound, can't delete chat message") - } else { - // adapter.notifyItemRemoved(index) - val eventLog = adapter.currentList[index] - addDeleteMessageTaskToQueue(eventLog, index) - } - } - } - RecyclerViewSwipeUtils( - ItemTouchHelper.RIGHT or ItemTouchHelper.LEFT, - swipeConfiguration, - swipeListener - ) - .attachToRecyclerView(binding.chatMessagesList) - - chatScrollListener = object : ChatScrollListener(layoutManager) { - override fun onLoadMore(totalItemsCount: Int) { - Log.i( - "[Chat Room] User has scrolled up far enough, load more items from history (currently there are $totalItemsCount messages displayed)" - ) - listViewModel.loadMoreData(totalItemsCount) - } - - override fun onScrolledUp() { - viewModel.isUserScrollingUp.value = true - } - - override fun onScrolledToEnd() { - viewModel.isUserScrollingUp.value = false - - val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly() - if (viewModel.unreadMessagesCount.value != 0 && - coreContext.notificationsManager.currentlyDisplayedChatRoomAddress == peerAddress - ) { - Log.i( - "[Chat Room] User has scrolled to the latest message, mark chat room as read" - ) - viewModel.chatRoom.markAsRead() - } - } - } - - chatSendingViewModel.textToSend.observe( - viewLifecycleOwner - ) { - chatSendingViewModel.onTextToSendChanged(it) - } - - chatSendingViewModel.isVoiceRecording.observe( - viewLifecycleOwner - ) { voiceRecording -> - // Keep screen on while recording voice message - if (voiceRecording) { - requireActivity().window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } else { - requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) - } - } - - chatSendingViewModel.requestRecordAudioPermissionEvent.observe( - viewLifecycleOwner - ) { - it.consume { - Log.i("[Chat Room] Asking for RECORD_AUDIO permission") - requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2) - } - } - - chatSendingViewModel.messageSentEvent.observe( - viewLifecycleOwner - ) { - it.consume { - Log.i("[Chat Room] Message sent") - // Reset this to ensure sent message will be visible - viewModel.isUserScrollingUp.value = false - } - } - - chatSendingViewModel.requestKeyboardHidingEvent.observe( - viewLifecycleOwner - ) { - it.consume { - (requireActivity() as MainActivity).hideKeyboard() - } - } - - listViewModel.events.observe( - viewLifecycleOwner - ) { events -> - adapter.setUnreadMessageCount( - viewModel.chatRoom.unreadMessagesCount, - viewModel.isUserScrollingUp.value == true - ) - adapter.submitList(events) - } - - listViewModel.messageUpdatedEvent.observe( - viewLifecycleOwner - ) { - it.consume { position -> - adapter.notifyItemChanged(position) - } - } - - listViewModel.requestWriteExternalStoragePermissionEvent.observe( - viewLifecycleOwner - ) { - it.consume { - requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1) - } - } - - adapter.deleteMessageEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatMessage -> - listViewModel.deleteMessage(chatMessage) - sharedViewModel.refreshChatRoomInListEvent.value = Event(true) - } - } - - adapter.resendMessageEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatMessage -> - listViewModel.resendMessage(chatMessage) - } - } - - adapter.forwardMessageEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatMessage -> - // Remove observer before setting the message to forward - // as we don't want to forward it in this chat room - sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner) - sharedViewModel.messageToForwardEvent.value = Event(chatMessage) - sharedViewModel.isPendingMessageForward.value = true - - if (sharedViewModel.isSlidingPaneSlideable.value == true) { - Log.i("[Chat Room] Forwarding message, going to chat rooms list") - goBack() - } else { - navigateToEmptyChatRoom() - } - } - } - - adapter.replyMessageEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatMessage -> - replyToChatMessage(chatMessage) - } - } - - adapter.showImdnForMessageEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatMessage -> - val args = Bundle() - args.putString("MessageId", chatMessage.messageId) - navigateToImdn(args) - } - } - - adapter.addSipUriToContactEvent.observe( - viewLifecycleOwner - ) { - it.consume { sipUri -> - Log.i("[Chat Room] Going to contacts list with SIP URI to add: $sipUri") - sharedViewModel.updateContactsAnimationsBasedOnDestination.value = - Event(R.id.masterChatRoomsFragment) - navigateToContacts(sipUri) - } - } - - adapter.openContentEvent.observe( - viewLifecycleOwner - ) { - it.consume { content -> - var path = content.filePath.orEmpty() - - if (path.isNotEmpty() && !File(path).exists()) { - Log.e("[Chat Room] File not found: $path") - (requireActivity() as MainActivity).showSnackBar( - R.string.chat_room_file_not_found - ) - } else { - if (path.isEmpty()) { - val name = content.name - if (!name.isNullOrEmpty()) { - val file = FileUtils.getFileStoragePath(name) - FileUtils.writeIntoFile(content.buffer, file) - path = file.absolutePath - content.filePath = path - Log.i( - "[Chat Room] Content file path was empty, created file from buffer at $path" - ) - } else if (content.isIcalendar) { - val filename = "conference.ics" - val file = FileUtils.getFileStoragePath(filename) - FileUtils.writeIntoFile(content.buffer, file) - path = file.absolutePath - content.filePath = path - Log.i( - "[Chat Room] Content file path was empty, created conference.ics from buffer at $path" - ) - } - } - - Log.i("[Chat Room] Opening file: $path") - sharedViewModel.contentToOpen.value = content - - if (corePreferences.useInAppFileViewerForNonEncryptedFiles || content.isFileEncrypted) { - val preventScreenshots = - viewModel.chatRoom.currentParams.isEncryptionEnabled - - val extension = FileUtils.getExtensionFromFileName(path) - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - when (FileUtils.getMimeType(mime)) { - FileUtils.MimeType.Image -> navigateToImageFileViewer( - preventScreenshots - ) - FileUtils.MimeType.Video -> navigateToVideoFileViewer( - preventScreenshots - ) - FileUtils.MimeType.Audio -> navigateToAudioFileViewer( - preventScreenshots - ) - FileUtils.MimeType.Pdf -> navigateToPdfFileViewer( - preventScreenshots - ) - FileUtils.MimeType.PlainText -> navigateToTextFileViewer( - preventScreenshots - ) - else -> { - if (content.isFileEncrypted) { - Log.w( - "[Chat Room] File is encrypted and can't be opened in one of our viewers..." - ) - showDialogForUserConsentBeforeExportingFileInThirdPartyApp( - content - ) - } else if (!FileUtils.openFileInThirdPartyApp( - requireActivity(), - path - ) - ) { - showDialogToSuggestOpeningFileAsText() - } - } - } - } else { - if (!FileUtils.openFileInThirdPartyApp(requireActivity(), path)) { - showDialogToSuggestOpeningFileAsText() - } - } - } - } - } - - adapter.urlClickEvent.observe( - viewLifecycleOwner - ) { - it.consume { url -> - val uri = Uri.parse(url) - val browserIntent = Intent( - Intent.ACTION_VIEW, - uri - ) - try { - startActivity(browserIntent) - } catch (se: SecurityException) { - Log.e("[Chat Room] Failed to start browser intent from uri [$uri]: $se") - } catch (anfe: ActivityNotFoundException) { - Log.e("[Chat Room] Failed to find app matching intent from uri [$uri]: $anfe") - } - } - } - - adapter.sipUriClickedEvent.observe( - viewLifecycleOwner - ) { - it.consume { sipUri -> - val args = Bundle() - args.putString("URI", sipUri) - args.putBoolean("Transfer", false) - // If auto start call setting is enabled, ignore it - args.putBoolean("SkipAutoCallStart", true) - navigateToDialer(args) - } - } - - adapter.callConferenceEvent.observe( - viewLifecycleOwner - ) { - it.consume { pair -> - navigateToConferenceWaitingRoom(pair.first, pair.second) - } - } - - adapter.scrollToChatMessageEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatMessage -> - var index: Int - var loadSteps = 0 - var expectedChildCount: Int - do { - val events = listViewModel.events.value.orEmpty() - expectedChildCount = events.size - Log.e("[Chat Room] expectedChildCount : $expectedChildCount") - val eventLog = events.find { eventLog -> - if (eventLog.eventLog.type == EventLog.Type.ConferenceChatMessage) { - (eventLog.data as ChatMessageData).chatMessage.messageId == chatMessage.messageId - } else { - false - } - } - index = events.indexOf(eventLog) - if (index == -1) { - loadSteps += 1 - listViewModel.loadMoreData(events.size) - } - } while (index == -1 && loadSteps < 5) - - if (index != -1) { - if (loadSteps == 0) { - scrollTo(index, true) - } else { - lifecycleScope.launch { - withContext(Dispatchers.Default) { - var retryCount = 0 - do { - // We have to wait for newly loaded items to be added to list before being able to scroll - delay(500) - retryCount += 1 - } while (layoutManager.itemCount != expectedChildCount && retryCount < 5) - - withContext(Dispatchers.Main) { - scrollTo(index, true) - } - } - } - } - } else { - Log.w("[Chat Room] Failed to find matching event!") - } - } - } - - adapter.showReactionsListEvent.observe( - viewLifecycleOwner - ) { - it.consume { message -> - val modalBottomSheet = ChatMessageReactionsListDialogFragment() - modalBottomSheet.setMessage(message) - modalBottomSheet.show( - parentFragmentManager, - ChatMessageReactionsListDialogFragment.TAG - ) - } - } - - adapter.errorEvent.observe( - viewLifecycleOwner - ) { - it.consume { message -> - (requireActivity() as MainActivity).showSnackBar(message) - } - } - - binding.setMenuClickListener { - showPopupMenu(chatRoom) - } - - binding.setMenuLongClickListener { - // Only show debug infos if debug mode is enabled - if (corePreferences.debugLogs) { - val alertDialog = MaterialAlertDialogBuilder(requireContext()) - - val messageBuilder = StringBuilder() - messageBuilder.append("Chat room id:\n") - messageBuilder.append(viewModel.chatRoom.peerAddress.asString()) - messageBuilder.append("\n") - messageBuilder.append("Local account:\n") - messageBuilder.append(viewModel.chatRoom.localAddress.asString()) - val message = messageBuilder.toString() - alertDialog.setMessage(message) - - alertDialog.setNeutralButton(R.string.chat_message_context_menu_copy_text) { - _, _ -> - val clipboard: ClipboardManager = - coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Chat room info", message) - clipboard.setPrimaryClip(clip) - } - - alertDialog.show() - true - } - false - } - - binding.setSecurityIconClickListener { - showParticipantsDevices() - } - - binding.setAttachFileClickListener { - if (PermissionHelper.get().hasReadExternalStoragePermission() || PermissionHelper.get().hasCameraPermission()) { - pickFile() - } else { - Log.i("[Chat Room] Asking for READ_EXTERNAL_STORAGE and CAMERA permissions") - Compatibility.requestReadExternalStorageAndCameraPermissions(this, 0) - } - } - - binding.setVoiceRecordingTouchListener { _, event -> - if (corePreferences.holdToRecordVoiceMessage) { - when (event.action) { - MotionEvent.ACTION_DOWN -> { - Log.i( - "[Chat Room] Start recording voice message as long as recording button is held" - ) - chatSendingViewModel.startVoiceRecording() - } - MotionEvent.ACTION_UP -> { - val voiceRecordingDuration = chatSendingViewModel.voiceRecordingDuration.value ?: 0 - if (voiceRecordingDuration < 1000) { - Log.w( - "[Chat Room] Voice recording button has been held for less than a second, considering miss click" - ) - chatSendingViewModel.cancelVoiceRecording() - (activity as MainActivity).showSnackBar( - R.string.chat_message_voice_recording_hold_to_record - ) - } else { - Log.i( - "[Chat Room] Voice recording button has been released, stop recording" - ) - chatSendingViewModel.stopVoiceRecording() - } - view.performClick() - } - } - } - false - } - - binding.footer.message.setControlEnterListener(object : RichEditTextSendListener { - override fun onControlEnterPressedAndReleased() { - Log.i("[Chat Room] Detected left control + enter key presses, sending message") - chatSendingViewModel.sendMessage() - } - }) - - binding.setCancelReplyToClickListener { - chatSendingViewModel.cancelReply() - } - - binding.setScrollToBottomClickListener { - scrollToFirstUnreadMessageOrBottom(true) - viewModel.isUserScrollingUp.value = false - } - - binding.setGroupCallListener { - showGroupCallDialog() - } - - if (textToShare?.isNotEmpty() == true) { - Log.i("[Chat Room] Found text to share") - chatSendingViewModel.textToSend.value = textToShare - } - if (filesToShare?.isNotEmpty() == true) { - lifecycleScope.launch { - withContext(Dispatchers.Main) { - chatSendingViewModel.attachingFileInProgress.value = true - for (filePath in filesToShare) { - val path = FileUtils.copyToLocalStorage(filePath) - Log.i( - "[Chat Room] Found [$filePath] file to share, matching path is [$path]" - ) - if (path != null) { - chatSendingViewModel.addAttachment(path) - } - } - chatSendingViewModel.attachingFileInProgress.value = false - } - } - } - - sharedViewModel.richContentUri.observe( - viewLifecycleOwner - ) { - it.consume { uri -> - Log.i("[Chat Room] Found rich content URI: $uri") - lifecycleScope.launch { - withContext(Dispatchers.Main) { - chatSendingViewModel.attachingFileInProgress.value = true - val path = FileUtils.getFilePath(requireContext(), uri) - Log.i("[Chat Room] Rich content URI [$uri] matching path is [$path]") - if (path != null) { - chatSendingViewModel.addAttachment(path) - } - chatSendingViewModel.attachingFileInProgress.value = false - } - } - } - } - - sharedViewModel.messageToForwardEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatMessage -> - Log.i("[Chat Room] Found message to transfer") - showForwardConfirmationDialog(chatMessage) - sharedViewModel.isPendingMessageForward.value = false - } - } - - startPostponedEnterTransition() - } - - override fun deleteItems(indexesOfItemToDelete: ArrayList) { - val list = ArrayList() - for (index in indexesOfItemToDelete) { - val eventLog = adapter.currentList[index] - list.add(eventLog) - } - listViewModel.deleteEventLogs(list) - sharedViewModel.refreshChatRoomInListEvent.value = Event(true) - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - var atLeastOneGranted = false - for (result in grantResults) { - atLeastOneGranted = atLeastOneGranted || result == PackageManager.PERMISSION_GRANTED - } - - when (requestCode) { - 0 -> { - if (atLeastOneGranted) { - pickFile() - } - } - 2 -> { - if (atLeastOneGranted) { - chatSendingViewModel.startVoiceRecording() - } - } - } - } - - override fun onResume() { - super.onResume() - - if (this::viewModel.isInitialized) { - // Prevent notifications for this chat room to be displayed - val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly() - coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress - - if (_adapter != null) { - try { - adapter.registerAdapterDataObserver(observer) - } catch (_: IllegalStateException) {} - } - - // Wait for items to be displayed - binding.chatMessagesList - .viewTreeObserver - .addOnGlobalLayoutListener(globalLayoutLayout) - } else { - Log.e( - "[Chat Room] Fragment resuming but viewModel lateinit property isn't initialized!" - ) - } - - (requireActivity() as MainActivity).addKeyboardVisibilityListener( - keyboardVisibilityListener - ) - } - - override fun onPause() { - if (::chatScrollListener.isInitialized) { - binding.chatMessagesList.removeOnScrollListener(chatScrollListener) - } - - binding.chatMessagesList - .viewTreeObserver.removeOnGlobalLayoutListener(globalLayoutLayout) - - if (_adapter != null) { - try { - adapter.unregisterAdapterDataObserver(observer) - } catch (_: IllegalStateException) {} - } - - // Conversation isn't visible anymore, any new message received in it will trigger a notification - coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null - - (requireActivity() as MainActivity).removeKeyboardVisibilityListener( - keyboardVisibilityListener - ) - - super.onPause() - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - chatSendingViewModel.attachFilePending.value = false - if (resultCode == Activity.RESULT_OK) { - lifecycleScope.launch { - withContext(Dispatchers.Main) { - chatSendingViewModel.attachingFileInProgress.value = true - for ( - fileToUploadPath in FileUtils.getFilesPathFromPickerIntent( - data, - chatSendingViewModel.temporaryFileUploadPath - ) - ) { - Log.i("[Chat Room] Found [$fileToUploadPath] file from intent") - chatSendingViewModel.addAttachment(fileToUploadPath) - } - chatSendingViewModel.attachingFileInProgress.value = false - } - } - } - } - - private fun enterEditionMode() { - listSelectionViewModel.isEditionEnabled.value = true - } - - private fun showParticipantsDevices() { - if (corePreferences.limeSecurityPopupEnabled) { - val dialogViewModel = DialogViewModel(getString(R.string.dialog_lime_security_message)) - dialogViewModel.showDoNotAskAgain = true - val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) - - dialogViewModel.showCancelButton { doNotAskAgain -> - if (doNotAskAgain) corePreferences.limeSecurityPopupEnabled = false - dialog.dismiss() - } - - val okLabel = if (viewModel.oneParticipantOneDevice) { - getString(R.string.dialog_call) - } else { - getString( - R.string.dialog_ok - ) - } - dialogViewModel.showOkButton( - { doNotAskAgain -> - if (doNotAskAgain) corePreferences.limeSecurityPopupEnabled = false - - val address = viewModel.onlyParticipantOnlyDeviceAddress - if (viewModel.oneParticipantOneDevice) { - if (address != null) { - coreContext.startCall(address, forceZRTP = true) - } - } else { - navigateToDevices() - } - - dialog.dismiss() - }, - okLabel - ) - - dialog.show() - } else { - val address = viewModel.onlyParticipantOnlyDeviceAddress - if (viewModel.oneParticipantOneDevice) { - if (address != null) { - coreContext.startCall(address, forceZRTP = true) - } - } else { - navigateToDevices() - } - } - } - - private fun showGroupInfo(chatRoom: ChatRoom) { - sharedViewModel.selectedGroupChatRoom.value = chatRoom - sharedViewModel.chatRoomParticipants.value = arrayListOf() - navigateToGroupInfo() - } - - private fun showEphemeralMessages() { - navigateToEphemeralInfo() - } - - private fun scheduleMeeting(chatRoom: ChatRoom) { - val participants = arrayListOf
() - for (participant in chatRoom.participants) { - participants.add(participant.address) - } - sharedViewModel.participantsListForNextScheduledMeeting.value = Event(participants) - navigateToConferenceScheduling() - } - - private fun showForwardConfirmationDialog(chatMessage: ChatMessage) { - val viewModel = DialogViewModel( - getString(R.string.chat_message_forward_confirmation_dialog) - ) - viewModel.iconResource = R.drawable.forward_message_default - viewModel.showIcon = true - val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) - - viewModel.showCancelButton { - Log.i("[Chat Room] Transfer cancelled") - dialog.dismiss() - } - - viewModel.showOkButton( - { - Log.i("[Chat Room] Transfer confirmed") - chatSendingViewModel.transferMessage(chatMessage) - dialog.dismiss() - }, - getString(R.string.chat_message_context_menu_forward) - ) - - dialog.show() - } - - private fun showPopupMenu(chatRoom: ChatRoom) { - val popupView: ChatRoomMenuBindingImpl = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.chat_room_menu, - null, - false - ) - val readOnly = chatRoom.isReadOnly - popupView.ephemeralEnabled = !readOnly - popupView.devicesEnabled = !readOnly - popupView.meetingEnabled = !readOnly - - val itemSize = AppUtils.getDimension(R.dimen.chat_room_popup_item_height).toInt() - var totalSize = itemSize * 8 - - val notificationsTurnedOff = viewModel.areNotificationsMuted() - if (notificationsTurnedOff) { - popupView.muteHidden = true - totalSize -= itemSize - } else { - popupView.unmuteHidden = true - totalSize -= itemSize - } - - if (viewModel.basicChatRoom || viewModel.oneToOneChatRoom) { - if (viewModel.contact.value != null) { - popupView.addToContactsHidden = true - } else { - popupView.goToContactHidden = true - - if (corePreferences.readOnlyNativeContacts) { - popupView.addToContactsHidden = true - totalSize -= itemSize - } - } - - popupView.meetingHidden = true - totalSize -= itemSize - } else { - popupView.addToContactsHidden = true - popupView.goToContactHidden = true - totalSize -= itemSize - } - - if (viewModel.basicChatRoom) { - popupView.groupInfoHidden = true - totalSize -= itemSize - popupView.devicesHidden = true - totalSize -= itemSize - popupView.ephemeralHidden = true - totalSize -= itemSize - } else { - if (!viewModel.encryptedChatRoom) { - popupView.devicesHidden = true - totalSize -= itemSize - popupView.ephemeralHidden = true - totalSize -= itemSize - } else { - if (viewModel.oneToOneChatRoom) { - popupView.groupInfoHidden = true - totalSize -= itemSize - } - - // If one participant one device, a click on security badge - // will directly start a call or show the dialog, so don't show this menu - if (viewModel.oneParticipantOneDevice) { - popupView.devicesHidden = true - totalSize -= itemSize - } - - if (viewModel.ephemeralChatRoom) { - if (chatRoom.currentParams.ephemeralMode == ChatRoom.EphemeralMode.AdminManaged) { - if (chatRoom.me?.isAdmin == false) { - Log.w( - "[Chat Room] Hiding ephemeral menu as mode is admin managed and we aren't admin" - ) - popupView.ephemeralHidden = true - totalSize -= itemSize - } - } - } - } - } - - // 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_room_popup_width).toInt(), - totalSize, - true - ) - // Elevation is for showing a shadow around the popup - popupWindow.elevation = 20f - - popupView.setGroupInfoListener { - showGroupInfo(chatRoom) - popupWindow.dismiss() - } - popupView.setDevicesListener { - showParticipantsDevices() - popupWindow.dismiss() - } - popupView.setEphemeralListener { - showEphemeralMessages() - popupWindow.dismiss() - } - popupView.setMeetingListener { - scheduleMeeting(chatRoom) - popupWindow.dismiss() - } - popupView.setEditionModeListener { - enterEditionMode() - popupWindow.dismiss() - } - popupView.setMuteListener { - viewModel.muteNotifications(true) - sharedViewModel.refreshChatRoomInListEvent.value = Event(true) - popupWindow.dismiss() - } - popupView.setUnmuteListener { - viewModel.muteNotifications(false) - sharedViewModel.refreshChatRoomInListEvent.value = Event(true) - popupWindow.dismiss() - } - popupView.setAddToContactsListener { - popupWindow.dismiss() - val copy = viewModel.getRemoteAddress()?.clone() - if (copy != null) { - copy.clean() - val address = copy.asStringUriOnly() - Log.i("[Chat Room] Creating contact with SIP URI: $address") - navigateToContacts(address) - } - } - popupView.setGoToContactListener { - popupWindow.dismiss() - val contactId = viewModel.contact.value?.refKey - if (contactId != null) { - Log.i("[Chat Room] Displaying native contact [$contactId]") - navigateToNativeContact(contactId) - } else { - val copy = viewModel.getRemoteAddress()?.clone() - if (copy != null) { - copy.clean() - val address = copy.asStringUriOnly() - Log.i("[Chat Room] Displaying friend with address [$address]") - navigateToFriend(address) - } - } - } - - popupWindow.showAsDropDown(binding.menu, 0, 0, Gravity.BOTTOM) - } - - private fun addDeleteMessageTaskToQueue(eventLog: EventLogData, position: Int) { - val task = lifecycleScope.launch { - delay(2800) // Duration of Snackbar.LENGTH_LONG - withContext(Dispatchers.Main) { - if (isActive) { - Log.i("[Chat Room] Message/event deletion task is still active, proceed") - val chatMessage = eventLog.eventLog.chatMessage - if (chatMessage != null) { - Log.i("[Chat Room] Deleting message $chatMessage at position $position") - listViewModel.deleteMessage(chatMessage) - } else { - Log.i("[Chat Room] Deleting event $eventLog at position $position") - listViewModel.deleteEventLogs(arrayListOf(eventLog)) - } - sharedViewModel.refreshChatRoomInListEvent.value = Event(true) - } - } - } - - (requireActivity() as MainActivity).showSnackBar( - R.string.chat_message_removal_info, - R.string.chat_message_abort_removal - ) { - Log.i( - "[Chat Room] Canceled message/event deletion task: $task for message/event at position $position" - ) - adapter.notifyItemRangeChanged(position, adapter.itemCount - position) - task.cancel() - } - } - - private fun scrollToFirstUnreadMessageOrBottom(smooth: Boolean): Boolean { - if (_adapter != null && adapter.itemCount > 0) { - val recyclerView = binding.chatMessagesList - - // Scroll to first unread message if any, unless we are already on it - val firstUnreadMessagePosition = adapter.getFirstUnreadMessagePosition() - val currentPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() - val indexToScrollTo = if (firstUnreadMessagePosition != -1 && firstUnreadMessagePosition != currentPosition) { - firstUnreadMessagePosition - } else { - adapter.itemCount - 1 - } - - Log.i( - "[Chat Room] Scrolling to position $indexToScrollTo, first unread message is at $firstUnreadMessagePosition" - ) - scrollTo(indexToScrollTo, smooth) - - if (firstUnreadMessagePosition == 0) { - // Return true only if all unread messages don't fit in the recyclerview height - return recyclerView.computeVerticalScrollRange() > recyclerView.height - } - } - return false - } - - private fun pickFile() { - chatSendingViewModel.attachFilePending.value = true - val intentsList = ArrayList() - - val pickerIntent = Intent(Intent.ACTION_GET_CONTENT) - pickerIntent.type = "*/*" - pickerIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - - if (PermissionHelper.get().hasCameraPermission()) { - // Allows to capture directly from the camera - val capturePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - val tempFileName = System.currentTimeMillis().toString() + ".jpeg" - val file = FileUtils.getFileStoragePath(tempFileName) - chatSendingViewModel.temporaryFileUploadPath = file - try { - val publicUri = FileProvider.getUriForFile( - requireContext(), - requireContext().getString(R.string.file_provider), - file - ) - capturePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, publicUri) - capturePictureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - capturePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - intentsList.add(capturePictureIntent) - - val captureVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) - intentsList.add(captureVideoIntent) - } catch (e: Exception) { - Log.e("[Chat Room] Failed to pick file: $e") - } - } - - val chooserIntent = - Intent.createChooser(pickerIntent, getString(R.string.chat_message_pick_file_dialog)) - chooserIntent.putExtra( - Intent.EXTRA_INITIAL_INTENTS, - intentsList.toArray(arrayOf()) - ) - - startActivityForResult(chooserIntent, 0) - } - - private fun showDialogToSuggestOpeningFileAsText() { - val dialogViewModel = DialogViewModel( - getString(R.string.dialog_try_open_file_as_text_body), - getString(R.string.dialog_try_open_file_as_text_title) - ) - val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) - - dialogViewModel.showCancelButton { - dialog.dismiss() - } - - dialogViewModel.showOkButton({ - dialog.dismiss() - navigateToTextFileViewer(true) - }) - - dialog.show() - } - - private fun showDialogForUserConsentBeforeExportingFileInThirdPartyApp(content: Content) { - val dialogViewModel = DialogViewModel( - getString(R.string.chat_message_cant_open_file_in_app_dialog_message), - getString(R.string.chat_message_cant_open_file_in_app_dialog_title) - ) - val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) - - dialogViewModel.showDeleteButton( - { - dialog.dismiss() - lifecycleScope.launch { - Log.i( - "[Chat Room] [VFS] Content is encrypted, requesting plain file path for file [${content.filePath}]" - ) - val plainFilePath = content.exportPlainFile() - if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) { - showDialogToSuggestOpeningFileAsText() - } - } - }, - getString(R.string.chat_message_cant_open_file_in_app_dialog_export_button) - ) - - dialogViewModel.showOkButton( - { - dialog.dismiss() - navigateToTextFileViewer(true) - }, - getString(R.string.chat_message_cant_open_file_in_app_dialog_open_as_text_button) - ) - - dialogViewModel.showCancelButton { - dialog.dismiss() - } - - dialog.show() - } - - private fun showGroupCallDialog() { - val dialogViewModel = DialogViewModel( - getString(R.string.conference_start_group_call_dialog_message), - getString(R.string.conference_start_group_call_dialog_title) - ) - val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) - - dialogViewModel.iconResource = R.drawable.icon_video_conf_incoming - dialogViewModel.showIcon = true - - dialogViewModel.showCancelButton { - dialog.dismiss() - } - - dialogViewModel.showOkButton( - { - dialog.dismiss() - viewModel.startGroupCall() - }, - getString(R.string.conference_start_group_call_dialog_ok_button) - ) - - dialog.show() - } - - private fun scrollTo(position: Int, smooth: Boolean = true) { - try { - if (smooth && corePreferences.enableAnimations) { - binding.chatMessagesList.smoothScrollToPosition(position) - } else { - binding.chatMessagesList.scrollToPosition(position) - } - } catch (iae: IllegalArgumentException) { - Log.e("[Chat Room] Can't scroll to position $position") - } - } - - private fun replyToChatMessage(chatMessage: ChatMessage) { - chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy() - chatSendingViewModel.pendingChatMessageToReplyTo.value = - ChatMessageData(chatMessage) - chatSendingViewModel.isPendingAnswer.value = true - - if (chatSendingViewModel.sendMessageEnabled.value == false) { - // Open keyboard - binding.footer.message.requestFocus() - (requireActivity() as MainActivity).showKeyboard() - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DevicesFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DevicesFragment.kt deleted file mode 100644 index a9f847813..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/DevicesFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt deleted file mode 100644 index ba4a3382e..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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() - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt deleted file mode 100644 index 3450a6ead..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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
() - 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() - - 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) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt deleted file mode 100644 index adadf75f5..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt deleted file mode 100644 index e41ff59db..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt +++ /dev/null @@ -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 . - */ -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() { - 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) { - val list = ArrayList() - 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) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/receivers/RichContentReceiver.kt b/app/src/main/java/org/linphone/activities/main/chat/receivers/RichContentReceiver.kt deleted file mode 100644 index e8dfeaee0..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/receivers/RichContentReceiver.kt +++ /dev/null @@ -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 . - */ -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 - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt deleted file mode 100644 index 9649aafc0..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return ChatMessageSendingViewModel(chatRoom) as T - } -} - -class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() { - var temporaryFileUploadPath: File? = null - - val attachments = MutableLiveData>() - - val attachFileEnabled = MutableLiveData() - - val attachFilePending = MutableLiveData() - - val sendMessageEnabled = MutableLiveData() - - val attachingFileInProgress = MutableLiveData() - - val isReadOnly = MutableLiveData() - - var textToSend = MutableLiveData() - - val isPendingAnswer = MutableLiveData() - - var pendingChatMessageToReplyTo = MutableLiveData() - - val requestRecordAudioPermissionEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val messageSentEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val voiceRecordingProgressBarMax = 10000 - - val isPendingVoiceRecord = MutableLiveData() - - val isVoiceRecording = MutableLiveData() - - val voiceRecordingDuration = MutableLiveData() - - val formattedDuration = MutableLiveData() - - val isPlayingVoiceRecording = MutableLiveData() - - val voiceRecordPlayingPosition = MutableLiveData() - - 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() - - val isEmojiPickerVisible = MutableLiveData() - - val isFileTransferAvailable = MutableLiveData() - - val requestKeyboardHidingEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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() - 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() - 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() - ) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt deleted file mode 100644 index cc3cdad16..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): 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>() - - val messageUpdatedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val requestWriteExternalStoragePermissionEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() { - override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array) { - 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) { - 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 = chatRoom.getHistoryRangeEvents( - totalItemsCount, - upperBound - ) - val list = arrayListOf() - for (eventLog in history) { - list.add(EventLogData(eventLog)) - } - list.addAll(events.value.orEmpty()) - events.value = list - } - } - - private fun addEvent(eventLog: EventLog) { - val list = arrayListOf() - 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 { - val list = arrayListOf() - 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) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt deleted file mode 100644 index 56d6ab24e..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt +++ /dev/null @@ -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 . - */ -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> by lazy { - MutableLiveData>() - } - - val createGroupChat = MutableLiveData() - - val isEncrypted = MutableLiveData() - - val waitForChatRoomCreation = MutableLiveData() - - val secureChatAvailable = MutableLiveData() - - 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 - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt deleted file mode 100644 index 24bab68c1..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return ChatRoomViewModel(chatRoom) as T - } -} - -class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactDataInterface { - override val contact: MutableLiveData = MutableLiveData() - override val displayName: MutableLiveData = MutableLiveData() - override val securityLevel: MutableLiveData = MutableLiveData() - override val showGroupChatAvatar: Boolean - get() = conferenceChatRoom && !oneToOneChatRoom - override val presenceStatus: MutableLiveData = MutableLiveData() - override val coroutineScope: CoroutineScope = viewModelScope - - val subject = MutableLiveData() - - val participants = MutableLiveData() - - val unreadMessagesCount = MutableLiveData() - - val remoteIsComposing = MutableLiveData() - - val composingList = MutableLiveData() - - val securityLevelIcon = MutableLiveData() - - val securityLevelContentDescription = MutableLiveData() - - val lastPresenceInfo = MutableLiveData() - - val ephemeralEnabled = MutableLiveData() - - 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 by lazy { - MutableLiveData() - } - - val isUserScrollingUp = MutableLiveData() - - var oneParticipantOneDevice: Boolean = false - - var onlyParticipantOnlyDeviceAddress: Address? = null - - val chatUnreadCountTranslateY = MutableLiveData() - - 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) { - 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() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt deleted file mode 100644 index 7f3915a7a..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomsListViewModel.kt +++ /dev/null @@ -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 . - */ -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>() - - val fileSharingPending = MutableLiveData() - - val textSharingPending = MutableLiveData() - - val forwardPending = MutableLiveData() - - val groupChatAvailable = MutableLiveData() - - val chatRoomIndexUpdatedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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() - 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() - 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 - ) { - 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) { - 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() - 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() - 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() - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt deleted file mode 100644 index 625ed9419..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/DevicesListViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return DevicesListViewModel(chatRoom) as T - } -} - -class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() { - val participants = MutableLiveData>() - - 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() - val me = chatRoom.me - if (me != null) list.add(DevicesListGroupData(me)) - for (participant in chatRoom.participants) { - list.add(DevicesListGroupData(participant)) - } - - participants.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt deleted file mode 100644 index 22da304cf..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/EphemeralViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return EphemeralViewModel(chatRoom) as T - } -} - -class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() { - val durationsList = MutableLiveData>() - - 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() - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt deleted file mode 100644 index ffeef03c8..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/GroupInfoViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return GroupInfoViewModel(chatRoom) as T - } -} - -class GroupInfoViewModel(val chatRoom: ChatRoom?) : MessageNotifierViewModel() { - val createdChatRoomEvent = MutableLiveData>() - val updatedChatRoomEvent = MutableLiveData>() - - val subject = MutableLiveData() - - val participants = MutableLiveData>() - - val isEncrypted = MutableLiveData() - - val isMeAdmin = MutableLiveData() - - val canLeaveGroup = MutableLiveData() - - val waitForChatRoomCreation = MutableLiveData() - - val meAdminChangedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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
(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() - 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(participantsToRemove.size) - participantsToRemove.toArray(toRemove) - chatRoom.removeParticipants(toRemove) - - // Added participants & new admins - val participantsToAdd = arrayListOf
() - 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
(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() - 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() - - if (chatRoom != null) { - for (participant in chatRoom.participants) { - list.add( - GroupInfoParticipantData( - GroupChatRoomMember( - participant.address, - participant.isAdmin, - participant.securityLevel, - canBeSetAdmin = true - ) - ) - ) - } - } - - participants.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnViewModel.kt deleted file mode 100644 index 2b9c72593..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ImdnViewModel.kt +++ /dev/null @@ -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 . - */ -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 create(modelClass: Class): T { - return ImdnViewModel(chatMessage) as T - } -} - -class ImdnViewModel(private val chatMessage: ChatMessage) : ViewModel() { - val participants = MutableLiveData>() - - 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() - - 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 - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt b/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt deleted file mode 100644 index fe08e43a0..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt +++ /dev/null @@ -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 . - */ -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) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt b/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt deleted file mode 100644 index 48662b95b..000000000 --- a/app/src/main/java/org/linphone/activities/main/chat/views/RichEditText.kt +++ /dev/null @@ -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 . - */ -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() -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/adapters/ScheduledConferencesAdapter.kt b/app/src/main/java/org/linphone/activities/main/conference/adapters/ScheduledConferencesAdapter.kt deleted file mode 100644 index 2d02ed599..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/adapters/ScheduledConferencesAdapter.kt +++ /dev/null @@ -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 . - */ -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( - selectionVM, - ConferenceInfoDiffCallback() -), - HeaderAdapter { - val copyAddressToClipboardEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val joinConferenceEvent: MutableLiveData>> by lazy { - MutableLiveData>>() - } - - val editConferenceEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val deleteConferenceInfoEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - 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() { - override fun areItemsTheSame( - oldItem: ScheduledConferenceData, - newItem: ScheduledConferenceData - ): Boolean { - return oldItem.conferenceInfo == newItem.conferenceInfo - } - - override fun areContentsTheSame( - oldItem: ScheduledConferenceData, - newItem: ScheduledConferenceData - ): Boolean { - return false - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt deleted file mode 100644 index dd8828aab..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/data/ConferenceSchedulingParticipantData.kt +++ /dev/null @@ -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 . - */ -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() - - 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) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/Duration.kt b/app/src/main/java/org/linphone/activities/main/conference/data/Duration.kt deleted file mode 100644 index 61a6ee696..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/data/Duration.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.conference.data - -class Duration(val value: Int, val display: String) : Comparable { - override fun toString(): String { - return display - } - - override fun compareTo(other: Duration): Int { - return value.compareTo(other.value) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt deleted file mode 100644 index 13d7aaa51..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/data/ScheduledConferenceData.kt +++ /dev/null @@ -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 . - */ -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() - val backgroundResId = MutableLiveData() - - val address = MutableLiveData() - val subject = MutableLiveData() - val description = MutableLiveData() - val time = MutableLiveData() - val date = MutableLiveData() - val duration = MutableLiveData() - val organizer = MutableLiveData() - val canEdit = MutableLiveData() - val participantsShort = MutableLiveData() - val participantsExpanded = MutableLiveData() - val showDuration = MutableLiveData() - val isConferenceCancelled = MutableLiveData() - val isBroadcast = MutableLiveData() - val speakersExpanded = MutableLiveData() - - 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"}" - ) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/data/TimeZoneData.kt b/app/src/main/java/org/linphone/activities/main/conference/data/TimeZoneData.kt deleted file mode 100644 index de9007e53..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/data/TimeZoneData.kt +++ /dev/null @@ -1,59 +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 . - */ -package org.linphone.activities.main.conference.data - -import java.util.* -import java.util.concurrent.TimeUnit -import kotlin.math.abs - -class TimeZoneData(timeZone: TimeZone) : Comparable { - val id: String = timeZone.id - private val hours: Long - private val minutes: Long - private val gmt: String - - init { - hours = TimeUnit.MILLISECONDS.toHours(timeZone.rawOffset.toLong()) - minutes = abs( - TimeUnit.MILLISECONDS.toMinutes(timeZone.rawOffset.toLong()) - - TimeUnit.HOURS.toMinutes(hours) - ) - - gmt = if (hours > 0) { - String.format("%s - GMT+%d:%02d", timeZone.id, hours, minutes) - } else { - String.format("%s - GMT%d:%02d", timeZone.id, hours, minutes) - } - } - - override fun toString(): String { - return gmt - } - - override fun compareTo(other: TimeZoneData): Int { - if (hours == other.hours) { - if (minutes == other.minutes) { - return id.compareTo(other.id) - } - return minutes.compareTo(other.minutes) - } - return hours.compareTo(other.hours) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingFragment.kt deleted file mode 100644 index c16a42c4f..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingFragment.kt +++ /dev/null @@ -1,129 +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 . - */ -package org.linphone.activities.main.conference.fragments - -import android.os.Bundle -import android.text.format.DateFormat.is24HourFormat -import android.view.View -import androidx.navigation.navGraphViewModels -import com.google.android.material.datepicker.CalendarConstraints -import com.google.android.material.datepicker.DateValidatorPointForward -import com.google.android.material.datepicker.MaterialDatePicker -import com.google.android.material.timepicker.MaterialTimePicker -import com.google.android.material.timepicker.TimeFormat -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel -import org.linphone.activities.navigateToParticipantsList -import org.linphone.core.Factory -import org.linphone.core.tools.Log -import org.linphone.databinding.ConferenceSchedulingFragmentBinding - -class ConferenceSchedulingFragment : GenericFragment() { - private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels( - R.id.conference_scheduling_nav_graph - ) - - override fun getLayoutId(): Int = R.layout.conference_scheduling_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.viewModel = viewModel - - sharedViewModel.participantsListForNextScheduledMeeting.observe( - viewLifecycleOwner - ) { - it.consume { participants -> - Log.i( - "[Conference Scheduling] Found participants (${participants.size}) to pre-populate for meeting schedule" - ) - viewModel.prePopulateParticipantsList(participants, true) - } - } - - sharedViewModel.addressOfConferenceInfoToEdit.observe( - viewLifecycleOwner - ) { - it.consume { address -> - val conferenceAddress = Factory.instance().createAddress(address) - if (conferenceAddress != null) { - Log.i( - "[Conference Scheduling] Trying to edit conference info using address: $address" - ) - val conferenceInfo = coreContext.core.findConferenceInformationFromUri( - conferenceAddress - ) - if (conferenceInfo != null) { - viewModel.populateFromConferenceInfo(conferenceInfo) - } else { - Log.e( - "[Conference Scheduling] Failed to find ConferenceInfo matching address: $address" - ) - } - } else { - Log.e("[Conference Scheduling] Failed to parse conference address: $address") - } - } - } - - binding.setNextClickListener { - navigateToParticipantsList() - } - - binding.setDatePickerClickListener { - val constraintsBuilder = - CalendarConstraints.Builder() - .setValidator(DateValidatorPointForward.now()) - val picker = - MaterialDatePicker.Builder.datePicker() - .setCalendarConstraints(constraintsBuilder.build()) - .setTitleText(R.string.conference_schedule_date) - .setSelection(viewModel.dateTimestamp) - .build() - picker.addOnPositiveButtonClickListener { - val selection = picker.selection - if (selection != null) { - viewModel.setDate(selection) - } - } - picker.show(requireFragmentManager(), "Date picker") - } - - binding.setTimePickerClickListener { - val isSystem24Hour = is24HourFormat(requireContext()) - val clockFormat = if (isSystem24Hour) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H - val picker = - MaterialTimePicker.Builder() - .setTimeFormat(clockFormat) - .setTitleText(R.string.conference_schedule_time) - .setHour(viewModel.hour) - .setMinute(viewModel.minutes) - .build() - picker.addOnPositiveButtonClickListener { - viewModel.setTime(picker.hour, picker.minute) - } - picker.show(requireFragmentManager(), "Time picker") - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingParticipantsListFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingParticipantsListFragment.kt deleted file mode 100644 index b59247d44..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingParticipantsListFragment.kt +++ /dev/null @@ -1,124 +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 . - */ -package org.linphone.activities.main.conference.fragments - -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.View -import androidx.navigation.navGraphViewModels -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.GenericFragment -import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel -import org.linphone.activities.navigateToSummary -import org.linphone.contact.ContactsSelectionAdapter -import org.linphone.core.tools.Log -import org.linphone.databinding.ConferenceSchedulingParticipantsListFragmentBinding -import org.linphone.utils.AppUtils -import org.linphone.utils.PermissionHelper - -class ConferenceSchedulingParticipantsListFragment : GenericFragment() { - private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels( - R.id.conference_scheduling_nav_graph - ) - private lateinit var adapter: ContactsSelectionAdapter - - override fun getLayoutId(): Int = R.layout.conference_scheduling_participants_list_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.viewModel = viewModel - - adapter = ContactsSelectionAdapter(viewLifecycleOwner) - 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.setNextClickListener { - navigateToSummary() - } - - viewModel.contactsList.observe( - viewLifecycleOwner - ) { - adapter.submitList(it) - } - viewModel.sipContactsSelected.observe( - viewLifecycleOwner - ) { - viewModel.applyFilter() - } - - viewModel.selectedAddresses.observe( - viewLifecycleOwner - ) { - adapter.updateSelectedAddresses(it) - } - viewModel.filter.observe( - viewLifecycleOwner - ) { - viewModel.applyFilter() - } - - adapter.selectedContact.observe( - viewLifecycleOwner - ) { - it.consume { searchResult -> - viewModel.toggleSelectionForSearchResult(searchResult) - } - } - - if (corePreferences.enableNativeAddressBookIntegration) { - if (!PermissionHelper.get().hasReadContactsPermission()) { - Log.i("[Conference 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, - grantResults: IntArray - ) { - if (requestCode == 0) { - val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED - if (granted) { - Log.i("[Conference Creation] READ_CONTACTS permission granted") - coreContext.fetchContacts() - } else { - Log.w("[Conference Creation] READ_CONTACTS permission denied") - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingSummaryFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingSummaryFragment.kt deleted file mode 100644 index 2358616ed..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceSchedulingSummaryFragment.kt +++ /dev/null @@ -1,72 +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 . - */ -package org.linphone.activities.main.conference.fragments - -import android.os.Bundle -import android.view.View -import androidx.navigation.navGraphViewModels -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.conference.viewmodels.ConferenceSchedulingViewModel -import org.linphone.activities.navigateToDialer -import org.linphone.activities.navigateToScheduledConferences -import org.linphone.databinding.ConferenceSchedulingSummaryFragmentBinding - -class ConferenceSchedulingSummaryFragment : GenericFragment() { - private val viewModel: ConferenceSchedulingViewModel by navGraphViewModels( - R.id.conference_scheduling_nav_graph - ) - - override fun getLayoutId(): Int = R.layout.conference_scheduling_summary_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.viewModel = viewModel - - viewModel.conferenceCreationCompletedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - if (viewModel.scheduleForLater.value == true) { - (requireActivity() as MainActivity).showSnackBar( - R.string.conference_schedule_info_created - ) - navigateToScheduledConferences() - } else { - navigateToDialer() - } - } - } - - viewModel.onMessageToNotifyEvent.observe( - viewLifecycleOwner - ) { - it.consume { messageId -> - (activity as MainActivity).showSnackBar(messageId) - } - } - - viewModel.computeParticipantsData() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt deleted file mode 100644 index 1e7c25d7a..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/fragments/ConferenceWaitingRoomFragment.kt +++ /dev/null @@ -1,204 +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 . - */ -package org.linphone.activities.main.conference.fragments - -import android.Manifest -import android.content.pm.PackageManager -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.main.MainActivity -import org.linphone.activities.main.conference.viewmodels.ConferenceWaitingRoomViewModel -import org.linphone.core.tools.Log -import org.linphone.databinding.ConferenceWaitingRoomFragmentBinding -import org.linphone.utils.PermissionHelper - -class ConferenceWaitingRoomFragment : GenericFragment() { - private lateinit var viewModel: ConferenceWaitingRoomViewModel - - override fun getLayoutId(): Int = R.layout.conference_waiting_room_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - viewModel = ViewModelProvider( - this - )[ConferenceWaitingRoomViewModel::class.java] - binding.viewModel = viewModel - - val conferenceSubject = arguments?.getString("Subject") - viewModel.subject.value = conferenceSubject - - val address = arguments?.getString("Address") - viewModel.findConferenceInfoByAddress(address) - - viewModel.cancelConferenceJoiningEvent.observe( - viewLifecycleOwner - ) { - it.consume { - if (viewModel.joinInProgress.value == true) { - val conferenceUri = arguments?.getString("Address") - val callToCancel = coreContext.core.calls.find { call -> - call.remoteAddress.asStringUriOnly() == conferenceUri - } - if (callToCancel != null) { - Log.i( - "[Conference Waiting Room] Call to conference server with URI [$conferenceUri] was started, terminate it" - ) - callToCancel.terminate() - } else { - Log.w( - "[Conference Waiting Room] Call to conference server with URI [$conferenceUri] wasn't found!" - ) - } - } - goBack() - } - } - - viewModel.joinConferenceEvent.observe( - viewLifecycleOwner - ) { - it.consume { callParams -> - val conferenceUri = arguments?.getString("Address") - if (conferenceUri != null) { - val conferenceAddress = coreContext.core.interpretUrl(conferenceUri, false) - if (conferenceAddress != null) { - Log.i( - "[Conference Waiting Room] Calling conference SIP URI: ${conferenceAddress.asStringUriOnly()}" - ) - coreContext.startCall(conferenceAddress, callParams) - } else { - Log.e( - "[Conference Waiting Room] Failed to parse conference SIP URI: $conferenceUri" - ) - } - } else { - Log.e( - "[Conference Waiting Room] Failed to find conference SIP URI in arguments" - ) - } - } - } - - viewModel.askPermissionEvent.observe( - viewLifecycleOwner - ) { - it.consume { permission -> - Log.i("[Conference Waiting Room] Asking for $permission permission") - requestPermissions(arrayOf(permission), 0) - } - } - - viewModel.leaveWaitingRoomEvent.observe( - viewLifecycleOwner - ) { - it.consume { - goBack() - } - } - - viewModel.onMessageToNotifyEvent.observe( - viewLifecycleOwner - ) { - it.consume { message -> - (activity as MainActivity).showSnackBar(message) - } - } - - viewModel.networkNotReachableEvent.observe( - viewLifecycleOwner - ) { - it.consume { - (activity as MainActivity).showSnackBar(R.string.call_error_network_unreachable) - } - } - - checkPermissions() - } - - override fun onResume() { - super.onResume() - - coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface - val enablePreview = viewModel.isVideoEnabled.value == true - if (enablePreview) { - Log.i("[Conference Waiting Room] Fragment is being resumed, enabling video preview") - } - coreContext.core.isVideoPreviewEnabled = enablePreview - } - - override fun onPause() { - Log.i("[Conference Waiting Room] Fragment is being paused, disabling video preview") - coreContext.core.isVideoPreviewEnabled = false - coreContext.core.nativePreviewWindowId = null - - super.onPause() - } - - private fun checkPermissions() { - val permissionsRequiredList = arrayListOf() - - if (!PermissionHelper.get().hasRecordAudioPermission()) { - Log.i("[Conference Waiting Room] Asking for RECORD_AUDIO permission") - permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO) - } - - if (!PermissionHelper.get().hasCameraPermission()) { - Log.i("[Conference Waiting Room] Asking for CAMERA permission") - permissionsRequiredList.add(Manifest.permission.CAMERA) - } - - if (permissionsRequiredList.isNotEmpty()) { - val permissionsRequired = arrayOfNulls(permissionsRequiredList.size) - permissionsRequiredList.toArray(permissionsRequired) - requestPermissions(permissionsRequired, 0) - } - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == 0) { - for (i in permissions.indices) { - when (permissions[i]) { - Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Conference Waiting Room] RECORD_AUDIO permission has been granted") - viewModel.enableMic() - } - Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Conference Waiting Room] CAMERA permission has been granted") - coreContext.core.reloadVideoDevices() - viewModel.enableVideo() - } - } - } - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/fragments/ScheduledConferencesFragment.kt b/app/src/main/java/org/linphone/activities/main/conference/fragments/ScheduledConferencesFragment.kt deleted file mode 100644 index a570af0c4..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/fragments/ScheduledConferencesFragment.kt +++ /dev/null @@ -1,188 +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 . - */ -package org.linphone.activities.main.conference.fragments - -import android.app.Dialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Bundle -import android.view.View -import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import org.linphone.R -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.conference.adapters.ScheduledConferencesAdapter -import org.linphone.activities.main.conference.data.ScheduledConferenceData -import org.linphone.activities.main.conference.viewmodels.ScheduledConferencesViewModel -import org.linphone.activities.main.fragments.MasterFragment -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.navigateToConferenceScheduling -import org.linphone.activities.navigateToConferenceWaitingRoom -import org.linphone.core.tools.Log -import org.linphone.databinding.ConferencesScheduledFragmentBinding -import org.linphone.utils.* - -class ScheduledConferencesFragment : MasterFragment() { - override val dialogConfirmationMessageBeforeRemoval = R.plurals.conference_scheduled_delete_dialog - private lateinit var listViewModel: ScheduledConferencesViewModel - - override fun getLayoutId(): Int = R.layout.conferences_scheduled_fragment - - private var deleteConferenceInfoDialog: Dialog? = null - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - listViewModel = ViewModelProvider( - this - )[ScheduledConferencesViewModel::class.java] - binding.viewModel = listViewModel - - _adapter = ScheduledConferencesAdapter(listSelectionViewModel, viewLifecycleOwner) - binding.conferenceInfoList.adapter = adapter - - val layoutManager = LinearLayoutManager(requireContext()) - binding.conferenceInfoList.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) - ) - val swipeListener = object : RecyclerViewSwipeListener { - override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {} - - override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) { - val index = viewHolder.bindingAdapterPosition - if (index < 0 || index >= adapter.currentList.size) { - Log.e( - "[Scheduled Conferences] Index is out of bound, can't delete conference info" - ) - } else { - val deletedConfInfo = adapter.currentList[index] - showConfInfoDeleteConfirmationDialog(deletedConfInfo, index) - } - } - } - RecyclerViewSwipeUtils(ItemTouchHelper.LEFT, swipeConfiguration, swipeListener) - .attachToRecyclerView(binding.conferenceInfoList) - - // Displays date header - val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) - binding.conferenceInfoList.addItemDecoration(headerItemDecoration) - - listViewModel.conferences.observe( - viewLifecycleOwner - ) { - adapter.submitList(it) - } - - adapter.copyAddressToClipboardEvent.observe( - viewLifecycleOwner - ) { - it.consume { address -> - val clipboard = - requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Conference address", address) - clipboard.setPrimaryClip(clip) - - (activity as MainActivity).showSnackBar( - R.string.conference_schedule_address_copied_to_clipboard - ) - } - } - - adapter.joinConferenceEvent.observe( - viewLifecycleOwner - ) { - it.consume { pair -> - navigateToConferenceWaitingRoom(pair.first, pair.second) - } - } - - adapter.editConferenceEvent.observe( - viewLifecycleOwner - ) { - it.consume { address -> - sharedViewModel.addressOfConferenceInfoToEdit.value = Event(address) - navigateToConferenceScheduling() - } - } - - adapter.deleteConferenceInfoEvent.observe( - viewLifecycleOwner - ) { - it.consume { data -> - showConfInfoDeleteConfirmationDialog(data, -1) - } - } - - binding.setNewConferenceClickListener { - navigateToConferenceScheduling() - } - } - - override fun deleteItems(indexesOfItemToDelete: ArrayList) { - val list = ArrayList() - for (index in indexesOfItemToDelete) { - val conferenceData = adapter.currentList[index] - list.add(conferenceData) - } - listViewModel.deleteConferencesInfo(list) - } - - private fun showConfInfoDeleteConfirmationDialog(data: ScheduledConferenceData, index: Int) { - val dialogViewModel = - DialogViewModel(AppUtils.getString(R.string.conference_scheduled_delete_one_dialog)) - deleteConferenceInfoDialog = - DialogUtils.getVoipDialog(requireContext(), dialogViewModel) - - dialogViewModel.showCancelButton( - { - if (index != -1) { - adapter.notifyItemChanged(index) - } - deleteConferenceInfoDialog?.dismiss() - }, - getString(R.string.dialog_cancel) - ) - - dialogViewModel.showDeleteButton( - { - listViewModel.deleteConferenceInfo(data) - deleteConferenceInfoDialog?.dismiss() - (requireActivity() as MainActivity).showSnackBar(R.string.conference_info_removed) - }, - getString(R.string.dialog_delete) - ) - - deleteConferenceInfoDialog?.show() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt deleted file mode 100644 index edbee3312..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceSchedulingViewModel.kt +++ /dev/null @@ -1,416 +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 . - */ -package org.linphone.activities.main.conference.viewmodels - -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import java.util.* -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.conference.data.ConferenceSchedulingParticipantData -import org.linphone.activities.main.conference.data.Duration -import org.linphone.activities.main.conference.data.TimeZoneData -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 -import org.linphone.utils.TimestampUtils - -class ConferenceSchedulingViewModel : ContactsSelectionViewModel() { - val subject = MutableLiveData() - val description = MutableLiveData() - - val scheduleForLater = MutableLiveData() - val isUpdate = MutableLiveData() - - val isBroadcastAllowed = MutableLiveData() - val mode = MutableLiveData() - val modesList: List - - val formattedDate = MutableLiveData() - val formattedTime = MutableLiveData() - - val isEncrypted = MutableLiveData() - - val sendInviteViaChat = MutableLiveData() - val sendInviteViaEmail = MutableLiveData() - - val participantsData = MutableLiveData>() - val speakersData = MutableLiveData>() - - val address = MutableLiveData
() - - val conferenceCreationInProgress = MutableLiveData() - - val conferenceCreationCompletedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val continueEnabled: MediatorLiveData = MediatorLiveData() - - var timeZone = MutableLiveData() - val timeZones: List = computeTimeZonesList() - - var duration = MutableLiveData() - val durationList: List = computeDurationList() - - var dateTimestamp: Long = System.currentTimeMillis() - var hour: Int = 0 - var minutes: Int = 0 - - private var confInfo: ConferenceInfo? = null - private val conferenceScheduler = coreContext.core.createConferenceScheduler() - - private val selectedSpeakersAddresses = MutableLiveData>() - - private val listener = object : ConferenceSchedulerListenerStub() { - override fun onStateChanged( - conferenceScheduler: ConferenceScheduler, - state: ConferenceScheduler.State - ) { - Log.i("[Conference Creation] Conference scheduler state is $state") - if (state == ConferenceScheduler.State.Ready) { - val conferenceAddress = conferenceScheduler.info?.uri - Log.i( - "[Conference Creation] Conference info created, address will be ${conferenceAddress?.asStringUriOnly()}" - ) - conferenceAddress ?: return - - address.value = conferenceAddress!! - - if (scheduleForLater.value == true) { - if (sendInviteViaChat.value == true) { - // Send conference info even when conf is not scheduled for later - // as the conference server doesn't invite participants automatically - Log.i( - "[Conference Creation] Scheduled conference is ready, sending invitations by chat" - ) - val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams() - conferenceScheduler.sendInvitations(chatRoomParams) - } else { - Log.i( - "[Conference Creation] Scheduled conference is ready, we were asked not to send invitations by chat so leaving fragment" - ) - conferenceCreationInProgress.value = false - conferenceCreationCompletedEvent.value = Event(true) - } - } else { - Log.i("[Conference Creation] Group call is ready, leaving fragment") - conferenceCreationInProgress.value = false - conferenceCreationCompletedEvent.value = Event(true) - } - } else if (state == ConferenceScheduler.State.Error) { - Log.e("[Conference Creation] Failed to create conference!") - conferenceCreationInProgress.value = false - onMessageToNotifyEvent.value = Event(R.string.conference_creation_failed) - } - } - - override fun onInvitationsSent( - conferenceScheduler: ConferenceScheduler, - failedInvitations: Array? - ) { - conferenceCreationInProgress.value = false - - if (failedInvitations?.isNotEmpty() == true) { - for (address in failedInvitations) { - Log.e( - "[Conference Creation] Conference information wasn't sent to participant ${address.asStringUriOnly()}" - ) - } - onMessageToNotifyEvent.value = Event( - R.string.conference_schedule_info_not_sent_to_participant - ) - } else { - Log.i( - "[Conference Creation] Conference information successfully sent to all participants" - ) - } - - val conferenceAddress = conferenceScheduler.info?.uri - if (conferenceAddress == null) { - Log.e("[Conference Creation] Conference address is null!") - } else { - conferenceCreationCompletedEvent.value = Event(true) - } - } - } - - init { - sipContactsSelected.value = true - - subject.value = "" - scheduleForLater.value = false - isUpdate.value = false - - isBroadcastAllowed.value = !corePreferences.disableBroadcastConference - modesList = arrayListOf( - AppUtils.getString(R.string.conference_schedule_mode_meeting), - AppUtils.getString(R.string.conference_schedule_mode_broadcast) - ) - mode.value = modesList.first() // Meeting by default - - isEncrypted.value = false - sendInviteViaChat.value = true - sendInviteViaEmail.value = false - - timeZone.value = timeZones.find { - it.id == TimeZone.getDefault().id - } - duration.value = durationList.find { - it.value == 60 - } - - continueEnabled.value = false - continueEnabled.addSource(subject) { - continueEnabled.value = allMandatoryFieldsFilled() - } - continueEnabled.addSource(scheduleForLater) { - continueEnabled.value = allMandatoryFieldsFilled() - } - continueEnabled.addSource(formattedDate) { - continueEnabled.value = allMandatoryFieldsFilled() - } - continueEnabled.addSource(formattedTime) { - continueEnabled.value = allMandatoryFieldsFilled() - } - - conferenceScheduler.addListener(listener) - } - - override fun onCleared() { - conferenceScheduler.removeListener(listener) - participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) - speakersData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) - - super.onCleared() - } - - fun prePopulateParticipantsList(participants: ArrayList
, isSchedule: Boolean) { - selectedAddresses.value = participants - scheduleForLater.value = isSchedule - } - - fun populateFromConferenceInfo(conferenceInfo: ConferenceInfo) { - // Pre-set data from existing conference info, used when editing an already scheduled broadcast or meeting - confInfo = conferenceInfo - - address.value = conferenceInfo.uri - subject.value = conferenceInfo.subject - description.value = conferenceInfo.description - isUpdate.value = true - - val dateTime = conferenceInfo.dateTime - val calendar = Calendar.getInstance() - calendar.timeInMillis = dateTime * 1000 - setDate(calendar.timeInMillis) - setTime(calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE)) - - val conferenceDuration = conferenceInfo.duration - duration.value = durationList.find { it.value == conferenceDuration } - scheduleForLater.value = conferenceDuration > 0 - - val participantsList = arrayListOf
() - val speakersList = arrayListOf
() - for (info in conferenceInfo.participantInfos) { - val participant = info.address - participantsList.add(participant) - if (info.role == Participant.Role.Speaker) { - speakersList.add(participant) - } - } - if (participantsList.count() == speakersList.count()) { - // All participants are speaker, this is a meeting, clear speakers - Log.i("[Conference Creation] Conference info is a meeting") - speakersList.clear() - mode.value = modesList.first() - } else { - Log.i("[Conference Creation] Conference info is a broadcast") - mode.value = modesList.last() - } - selectedAddresses.value = participantsList - selectedSpeakersAddresses.value = speakersList - - computeParticipantsData() - } - - fun toggleSchedule() { - scheduleForLater.value = scheduleForLater.value == false - } - - fun setDate(d: Long) { - dateTimestamp = d - formattedDate.value = TimestampUtils.dateToString(dateTimestamp, false) - } - - fun setTime(h: Int, m: Int) { - hour = h - minutes = m - formattedTime.value = TimestampUtils.timeToString(hour, minutes) - } - - fun updateEncryption(enable: Boolean) { - isEncrypted.value = enable - } - - fun computeParticipantsData() { - participantsData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) - speakersData.value.orEmpty().forEach(ConferenceSchedulingParticipantData::destroy) - - val participantsList = arrayListOf() - val speakersList = arrayListOf() - - for (address in selectedAddresses.value.orEmpty()) { - val isSpeaker = address in selectedSpeakersAddresses.value.orEmpty() - val data = ConferenceSchedulingParticipantData( - address, - showLimeBadge = isEncrypted.value == true, - showBroadcastControls = isModeBroadcastCurrentlySelected(), - speaker = isSpeaker, - onAddedToSpeakers = { data -> - Log.i( - "[Conference Creation] Participant [${address.asStringUriOnly()}] added to speakers" - ) - val participants = arrayListOf() - participants.addAll(participantsData.value.orEmpty()) - participants.remove(data) - participantsData.value = participants - - val speakers = arrayListOf() - speakers.addAll(speakersData.value.orEmpty()) - speakers.add(data) - speakersData.value = speakers - }, - onRemovedFromSpeakers = { data -> - Log.i( - "[Conference Creation] Participant [${address.asStringUriOnly()}] removed from speakers" - ) - val speakers = arrayListOf() - speakers.addAll(speakersData.value.orEmpty()) - speakers.remove(data) - speakersData.value = speakers - - val participants = arrayListOf() - participants.addAll(participantsData.value.orEmpty()) - participants.add(data) - participantsData.value = participants - } - ) - - if (isSpeaker) { - speakersList.add(data) - } else { - participantsList.add(data) - } - } - - participantsData.value = participantsList - speakersData.value = speakersList - } - - fun createConference() { - val participantsCount = selectedAddresses.value.orEmpty().size - if (participantsCount == 0) { - Log.e("[Conference Creation] Couldn't create conference without any participant!") - return - } - - conferenceCreationInProgress.value = true - val core = coreContext.core - val localAccount = core.defaultAccount - val localAddress = localAccount?.params?.identityAddress - - val conferenceInfo = if (isUpdate.value == true) { - confInfo?.clone() ?: Factory.instance().createConferenceInfo() - } else { - Factory.instance().createConferenceInfo() - } - conferenceInfo.organizer = localAddress - conferenceInfo.subject = subject.value - conferenceInfo.description = description.value - - val participants = arrayOfNulls(selectedAddresses.value.orEmpty().size) - var index = 0 - val isBroadcast = isModeBroadcastCurrentlySelected() - for (participant in participantsData.value.orEmpty()) { - val info = Factory.instance().createParticipantInfo(participant.sipAddress) - // For meetings, all participants must have Speaker role - info?.role = if (isBroadcast) Participant.Role.Listener else Participant.Role.Speaker - participants[index] = info - index += 1 - } - for (speaker in speakersData.value.orEmpty()) { - val info = Factory.instance().createParticipantInfo(speaker.sipAddress) - info?.role = Participant.Role.Speaker - participants[index] = info - index += 1 - } - conferenceInfo.setParticipantInfos(participants) - - if (scheduleForLater.value == true) { - val startTime = getConferenceStartTimestamp() - conferenceInfo.dateTime = startTime - val duration = duration.value?.value ?: 0 - conferenceInfo.duration = duration - } - - confInfo = conferenceInfo - conferenceScheduler.account = localAccount - // Will trigger the conference creation/update automatically - conferenceScheduler.info = conferenceInfo - } - - fun isModeBroadcastCurrentlySelected(): Boolean { - return mode.value == AppUtils.getString(R.string.conference_schedule_mode_broadcast) - } - - private fun computeTimeZonesList(): List { - return TimeZone.getAvailableIDs().map { id -> TimeZoneData(TimeZone.getTimeZone(id)) }.toList().sorted() - } - - private fun computeDurationList(): List { - // Duration value is in minutes as according to conferenceInfo.setDuration() doc - return arrayListOf(Duration(30, "30min"), Duration(60, "1h"), Duration(120, "2h")) - } - - private fun allMandatoryFieldsFilled(): Boolean { - return !subject.value.isNullOrEmpty() && - ( - scheduleForLater.value == false || - ( - !formattedDate.value.isNullOrEmpty() && - !formattedTime.value.isNullOrEmpty() - ) - ) - } - - private fun getConferenceStartTimestamp(): Long { - val calendar = Calendar.getInstance( - TimeZone.getTimeZone(timeZone.value?.id ?: TimeZone.getDefault().id) - ) - calendar.timeInMillis = dateTimestamp - calendar.set(Calendar.HOUR_OF_DAY, hour) - calendar.set(Calendar.MINUTE, minutes) - return calendar.timeInMillis / 1000 // Linphone expects a time_t (so in seconds) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt deleted file mode 100644 index ba194278d..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ConferenceWaitingRoomViewModel.kt +++ /dev/null @@ -1,499 +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 . - */ -package org.linphone.activities.main.conference.viewmodels - -import android.Manifest -import android.animation.ValueAnimator -import androidx.lifecycle.MutableLiveData -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.viewmodels.MessageNotifierViewModel -import org.linphone.activities.voip.ConferenceDisplayMode -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.* -import org.linphone.utils.Event - -class ConferenceWaitingRoomViewModel : MessageNotifierViewModel() { - val subject = MutableLiveData() - - val isMicrophoneMuted = MutableLiveData() - - val audioRoutesEnabled = MutableLiveData() - - val audioRoutesSelected = MutableLiveData() - - val isSpeakerSelected = MutableLiveData() - - val isBluetoothHeadsetSelected = MutableLiveData() - - val layoutMenuSelected = MutableLiveData() - - val selectedLayout = MutableLiveData() - - val isVideoAvailable = MutableLiveData() - - val isVideoEnabled = MutableLiveData() - - val isSwitchCameraAvailable = MutableLiveData() - - val isLowBandwidth = MutableLiveData() - - val joinInProgress = MutableLiveData() - - val networkReachable = MutableLiveData() - - val isConferenceBroadcastWithListenerRole = MutableLiveData() - - val askPermissionEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val cancelConferenceJoiningEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val joinConferenceEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val leaveWaitingRoomEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val networkNotReachableEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val audioRoutesMenuTranslateY = MutableLiveData() - private val audioRoutesMenuAnimator: ValueAnimator by lazy { - ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.voip_audio_routes_menu_translate_y), 0f).apply { - addUpdateListener { - val value = it.animatedValue as Float - audioRoutesMenuTranslateY.value = value - } - duration = if (corePreferences.enableAnimations) 500 else 0 - } - } - - val conferenceLayoutMenuTranslateY = MutableLiveData() - private val conferenceLayoutMenuAnimator: ValueAnimator by lazy { - ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.voip_audio_routes_menu_translate_y), 0f).apply { - addUpdateListener { - val value = it.animatedValue as Float - conferenceLayoutMenuTranslateY.value = value - } - duration = if (corePreferences.enableAnimations) 500 else 0 - } - } - - val hideVideo = corePreferences.disableVideo - - private val callParams: CallParams = coreContext.core.createCallParams(null)!! - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onAudioDevicesListUpdated(core: Core) { - Log.i("[Conference Waiting Room] Audio devices list updated") - onAudioDevicesListUpdated() - } - - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State?, - message: String - ) { - when (state) { - Call.State.End -> { - Log.i("[Conference Waiting Room] Call has ended, leaving waiting room fragment") - leaveWaitingRoomEvent.value = Event(true) - } - Call.State.Error -> { - Log.w( - "[Conference Waiting Room] Call has failed, leaving waiting room fragment" - ) - leaveWaitingRoomEvent.value = Event(true) - } - else -> {} - } - } - - override fun onConferenceStateChanged( - core: Core, - conference: Conference, - state: Conference.State? - ) { - if (state == Conference.State.Created) { - Log.i( - "[Conference Waiting Room] Conference has been created, leaving waiting room fragment" - ) - leaveWaitingRoomEvent.value = Event(true) - } - } - - override fun onNetworkReachable(core: Core, reachable: Boolean) { - Log.i("[Conference Waiting Room] Network reachability changed: [$reachable]") - networkReachable.value = reachable - if (!reachable) { - networkNotReachableEvent.value = Event(true) - } - } - } - - init { - val core = coreContext.core - core.addListener(listener) - - audioRoutesMenuTranslateY.value = AppUtils.getDimension( - R.dimen.voip_audio_routes_menu_translate_y - ) - conferenceLayoutMenuTranslateY.value = AppUtils.getDimension( - R.dimen.voip_audio_routes_menu_translate_y - ) - - val reachable = core.isNetworkReachable - networkReachable.value = reachable - if (!reachable) { - networkNotReachableEvent.value = Event(true) - } - - callParams.isMicEnabled = PermissionHelper.get().hasRecordAudioPermission() && coreContext.core.isMicEnabled - Log.i( - "[Conference Waiting Room] Microphone will be ${if (callParams.isMicEnabled) "enabled" else "muted"}" - ) - updateMicState() - - callParams.isVideoEnabled = isVideoAvailableInCore() - callParams.videoDirection = if (core.videoActivationPolicy.automaticallyInitiate) MediaDirection.SendRecv else MediaDirection.RecvOnly - updateVideoState() - - isLowBandwidth.value = false - if (LinphoneUtils.checkIfNetworkHasLowBandwidth(coreContext.context)) { - Log.w( - "[Conference Waiting Room] Enabling low bandwidth mode, forcing audio only layout!" - ) - callParams.isLowBandwidthEnabled = true - callParams.isVideoEnabled = false - callParams.videoDirection = MediaDirection.Inactive - isLowBandwidth.value = true - - updateVideoState() - onMessageToNotifyEvent.value = Event(R.string.conference_low_bandwidth) - } - - layoutMenuSelected.value = false - when (core.defaultConferenceLayout) { - Conference.Layout.Grid -> setMosaicLayout() - else -> setActiveSpeakerLayout() - } - - if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) { - setBluetoothAudioRoute() - } else if (isVideoAvailableInCore() && isVideoEnabled.value == true) { - setSpeakerAudioRoute() - } else { - setEarpieceAudioRoute() - } - onAudioDevicesListUpdated() - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - - super.onCleared() - } - - fun findConferenceInfoByAddress(stringAddress: String?) { - if (stringAddress != null) { - val address = Factory.instance().createAddress(stringAddress) - if (address != null) { - val conferenceInfo = coreContext.core.findConferenceInformationFromUri(address) - if (conferenceInfo != null) { - val myself = conferenceInfo.participantInfos.find { - it.address.asStringUriOnly() == coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly() - } - if (myself != null) { - Log.i( - "[Conference Waiting Room] Found our participant, it's role is [${myself.role}]" - ) - val areWeListener = myself.role == Participant.Role.Listener - isConferenceBroadcastWithListenerRole.value = areWeListener - } else { - Log.e( - "[Conference Waiting Room] Failed to find ourselves in participants info" - ) - } - } else { - Log.e( - "[Conference Waiting Room] Failed to find conference info using address [$stringAddress]" - ) - } - } - } else { - Log.e("[Conference Waiting Room] Can't find conference info using null address!") - } - } - - fun cancel() { - cancelConferenceJoiningEvent.value = Event(true) - } - - fun start() { - // Hide menus - audioRoutesSelected.value = false - layoutMenuSelected.value = false - - joinInProgress.value = true - joinConferenceEvent.value = Event(callParams) - } - - fun toggleMuteMicrophone() { - if (!PermissionHelper.get().hasRecordAudioPermission()) { - askPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO) - return - } - - callParams.isMicEnabled = !callParams.isMicEnabled - Log.i( - "[Conference Waiting Room] Microphone will be ${if (callParams.isMicEnabled) "enabled" else "muted"}" - ) - updateMicState() - } - - fun enableMic() { - Log.i("[Conference Waiting Room] Microphone will be enabled") - callParams.isMicEnabled = true - updateMicState() - } - - fun toggleSpeaker() { - if (isSpeakerSelected.value == true) { - setEarpieceAudioRoute() - } else { - setSpeakerAudioRoute() - } - } - - fun toggleAudioRoutesMenu() { - audioRoutesSelected.value = audioRoutesSelected.value != true - if (audioRoutesSelected.value == true) { - audioRoutesMenuAnimator.start() - } else { - audioRoutesMenuAnimator.reverse() - } - } - - fun setBluetoothAudioRoute() { - Log.i("[Conference Waiting Room] Set default output audio device to Bluetooth") - callParams.outputAudioDevice = coreContext.core.audioDevices.find { - it.type == AudioDevice.Type.Bluetooth && it.hasCapability( - AudioDevice.Capabilities.CapabilityPlay - ) - } - callParams.inputAudioDevice = coreContext.core.audioDevices.find { - it.type == AudioDevice.Type.Bluetooth && it.hasCapability( - AudioDevice.Capabilities.CapabilityRecord - ) - } - updateAudioRouteState() - - if (audioRoutesSelected.value == true) { - audioRoutesSelected.value = false - audioRoutesMenuAnimator.reverse() - } - } - - fun setSpeakerAudioRoute() { - Log.i("[Conference Waiting Room] Set default output audio device to Speaker") - callParams.outputAudioDevice = coreContext.core.audioDevices.find { - it.type == AudioDevice.Type.Speaker && it.hasCapability( - AudioDevice.Capabilities.CapabilityPlay - ) - } - callParams.inputAudioDevice = coreContext.core.audioDevices.find { - it.type == AudioDevice.Type.Microphone && it.hasCapability( - AudioDevice.Capabilities.CapabilityRecord - ) - } - updateAudioRouteState() - - if (audioRoutesSelected.value == true) { - audioRoutesSelected.value = false - audioRoutesMenuAnimator.reverse() - } - } - - fun setEarpieceAudioRoute() { - Log.i("[Conference Waiting Room] Set default output audio device to Earpiece") - callParams.outputAudioDevice = coreContext.core.audioDevices.find { - it.type == AudioDevice.Type.Earpiece && it.hasCapability( - AudioDevice.Capabilities.CapabilityPlay - ) - } - callParams.inputAudioDevice = coreContext.core.audioDevices.find { - it.type == AudioDevice.Type.Microphone && it.hasCapability( - AudioDevice.Capabilities.CapabilityRecord - ) - } - updateAudioRouteState() - - if (audioRoutesSelected.value == true) { - audioRoutesSelected.value = false - audioRoutesMenuAnimator.reverse() - } - } - - fun toggleLayoutMenu() { - layoutMenuSelected.value = layoutMenuSelected.value != true - if (layoutMenuSelected.value == true) { - conferenceLayoutMenuAnimator.start() - } else { - conferenceLayoutMenuAnimator.reverse() - } - } - - fun setMosaicLayout() { - Log.i("[Conference Waiting Room] Set default layout to Mosaic") - - callParams.conferenceVideoLayout = Conference.Layout.Grid - callParams.isVideoEnabled = isVideoAvailableInCore() - - updateLayout() - updateVideoState() - - layoutMenuSelected.value = false - conferenceLayoutMenuAnimator.reverse() - } - - fun setActiveSpeakerLayout() { - Log.i("[Conference Waiting Room] Set default layout to ActiveSpeaker") - - callParams.conferenceVideoLayout = Conference.Layout.ActiveSpeaker - callParams.isVideoEnabled = isVideoAvailableInCore() - - updateLayout() - updateVideoState() - - layoutMenuSelected.value = false - conferenceLayoutMenuAnimator.reverse() - } - - fun setAudioOnlyLayout() { - Log.i("[Conference Waiting Room] Set default layout to AudioOnly, disabling video in call") - callParams.isVideoEnabled = false - - updateLayout() - updateVideoState() - - layoutMenuSelected.value = false - conferenceLayoutMenuAnimator.reverse() - } - - fun toggleVideo() { - if (!PermissionHelper.get().hasCameraPermission()) { - askPermissionEvent.value = Event(Manifest.permission.CAMERA) - return - } - callParams.isVideoEnabled = isVideoAvailableInCore() - callParams.videoDirection = if (callParams.videoDirection == MediaDirection.SendRecv) MediaDirection.RecvOnly else MediaDirection.SendRecv - updateVideoState() - } - - fun enableVideo() { - callParams.isVideoEnabled = isVideoAvailableInCore() - callParams.videoDirection = MediaDirection.SendRecv - updateVideoState() - } - - fun switchCamera() { - Log.i("[Conference Waiting Room] Switching camera") - coreContext.switchCamera() - } - - private fun updateMicState() { - isMicrophoneMuted.value = !callParams.isMicEnabled - } - - private fun onAudioDevicesListUpdated() { - val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable() - if (!bluetoothDeviceAvailable && audioRoutesEnabled.value == true) { - Log.w( - "[Conference Waiting Room] Bluetooth device no longer available, switching back to default microphone & earpiece/speaker" - ) - } - audioRoutesEnabled.value = bluetoothDeviceAvailable - - if (!bluetoothDeviceAvailable) { - audioRoutesSelected.value = false - if (isBluetoothHeadsetSelected.value == true) { - for (audioDevice in coreContext.core.audioDevices) { - if (isVideoEnabled.value == true) { - if (audioDevice.type == AudioDevice.Type.Speaker) { - callParams.outputAudioDevice = audioDevice - } - } else { - if (audioDevice.type == AudioDevice.Type.Earpiece) { - callParams.outputAudioDevice = audioDevice - } - } - if (audioDevice.type == AudioDevice.Type.Microphone) { - callParams.inputAudioDevice = audioDevice - } - } - } - } - - updateAudioRouteState() - } - - private fun updateAudioRouteState() { - val outputDeviceType = callParams.outputAudioDevice?.type - isSpeakerSelected.value = outputDeviceType == AudioDevice.Type.Speaker - isBluetoothHeadsetSelected.value = outputDeviceType == AudioDevice.Type.Bluetooth - } - - private fun updateLayout() { - if (!callParams.isVideoEnabled) { - selectedLayout.value = ConferenceDisplayMode.AUDIO_ONLY - } else { - selectedLayout.value = when (callParams.conferenceVideoLayout) { - Conference.Layout.Grid -> ConferenceDisplayMode.GRID - else -> ConferenceDisplayMode.ACTIVE_SPEAKER - } - } - } - - private fun updateVideoState() { - isVideoAvailable.value = callParams.isVideoEnabled - isVideoEnabled.value = callParams.isVideoEnabled && callParams.videoDirection == MediaDirection.SendRecv - Log.i( - "[Conference Waiting Room] Video will be ${if (callParams.isVideoEnabled) "enabled" else "disabled"} with direction ${callParams.videoDirection}" - ) - - isSwitchCameraAvailable.value = callParams.isVideoEnabled && coreContext.showSwitchCameraButton() - coreContext.core.isVideoPreviewEnabled = isVideoEnabled.value == true - } - - private fun isVideoAvailableInCore(): Boolean { - val core = coreContext.core - return core.isVideoCaptureEnabled || core.isVideoPreviewEnabled - } -} diff --git a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ScheduledConferencesViewModel.kt b/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ScheduledConferencesViewModel.kt deleted file mode 100644 index c8acab844..000000000 --- a/app/src/main/java/org/linphone/activities/main/conference/viewmodels/ScheduledConferencesViewModel.kt +++ /dev/null @@ -1,160 +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 . - */ -package org.linphone.activities.main.conference.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.main.conference.data.ScheduledConferenceData -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.LinphoneUtils - -class ScheduledConferencesViewModel : ViewModel() { - val conferences = MutableLiveData>() - - val showTerminated = MutableLiveData() - - private val conferenceScheduler: ConferenceScheduler by lazy { - val scheduler = coreContext.core.createConferenceScheduler() - scheduler.addListener(conferenceListener) - scheduler - } - - private val listener = object : CoreListenerStub() { - override fun onConferenceInfoReceived(core: Core, conferenceInfo: ConferenceInfo) { - Log.i("[Scheduled Conferences] New conference info received") - computeConferenceInfoList() - } - } - - private val conferenceListener = object : ConferenceSchedulerListenerStub() { - override fun onStateChanged( - conferenceScheduler: ConferenceScheduler, - state: ConferenceScheduler.State - ) { - Log.i("[Scheduled Conferences] Conference scheduler state is $state") - if (state == ConferenceScheduler.State.Ready) { - Log.i( - "[Scheduled Conferences] Conference ${conferenceScheduler.info?.subject} cancelled" - ) - val chatRoomParams = LinphoneUtils.getConferenceInvitationsChatRoomParams() - conferenceScheduler.sendInvitations(chatRoomParams) // Send cancel ICS - } - } - - override fun onInvitationsSent( - conferenceScheduler: ConferenceScheduler, - failedInvitations: Array? - ) { - if (failedInvitations?.isNotEmpty() == true) { - for (address in failedInvitations) { - Log.e( - "[Scheduled Conferences] Conference cancelled ICS wasn't sent to participant ${address.asStringUriOnly()}" - ) - } - } else { - Log.i( - "[Scheduled Conferences] Conference cancelled ICS successfully sent to all participants" - ) - } - } - } - - init { - coreContext.core.addListener(listener) - - showTerminated.value = false - - computeConferenceInfoList() - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - conferences.value.orEmpty().forEach(ScheduledConferenceData::destroy) - - super.onCleared() - } - - fun applyFilter() { - computeConferenceInfoList() - } - - fun deleteConferenceInfo(data: ScheduledConferenceData) { - val conferenceInfoList = arrayListOf() - - conferenceInfoList.addAll(conferences.value.orEmpty()) - conferenceInfoList.remove(data) - - if (data.conferenceInfo.state != ConferenceInfo.State.Cancelled && data.canEdit.value == true) { - Log.i("[Scheduled Conferences] Cancelling conference ${data.conferenceInfo.subject}") - conferenceScheduler.cancelConference(data.conferenceInfo) - } - - data.delete() - data.destroy() - conferences.value = conferenceInfoList - } - - fun deleteConferencesInfo(toRemoveList: List) { - val conferenceInfoList = arrayListOf() - - for (confInfo in conferences.value.orEmpty()) { - if (confInfo in toRemoveList) { - confInfo.delete() - confInfo.destroy() - } else { - conferenceInfoList.add(confInfo) - } - } - - conferences.value = conferenceInfoList - } - - private fun computeConferenceInfoList() { - conferences.value.orEmpty().forEach(ScheduledConferenceData::destroy) - - val conferencesList = arrayListOf() - - val now = System.currentTimeMillis() / 1000 // Linphone uses time_t in seconds - - if (showTerminated.value == true) { - for (conferenceInfo in coreContext.core.conferenceInformationList) { - if (conferenceInfo.duration == 0) continue // This isn't a scheduled conference, don't display it - val limit = conferenceInfo.dateTime + conferenceInfo.duration - if (limit >= now) continue // This isn't a terminated conference, don't display it - val data = ScheduledConferenceData(conferenceInfo, true) - conferencesList.add(0, data) // Keep terminated meetings list in reverse order to always display most recent on top - } - } else { - val oneHourAgo = now - 7200 // Show all conferences from 2 hours ago and forward - for (conferenceInfo in coreContext.core.getConferenceInformationListAfterTime( - oneHourAgo - )) { - if (conferenceInfo.duration == 0) continue // This isn't a scheduled conference, don't display it - val data = ScheduledConferenceData(conferenceInfo, false) - conferencesList.add(data) - } - } - - conferences.value = conferencesList - Log.i("[Scheduled Conferences] Found ${conferencesList.size} future conferences") - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt deleted file mode 100644 index 5d503f94f..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/adapters/ContactsListAdapter.kt +++ /dev/null @@ -1,151 +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 . - */ -package org.linphone.activities.main.contact.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.contact.viewmodels.ContactViewModel -import org.linphone.activities.main.viewmodels.ListTopBarViewModel -import org.linphone.core.Friend -import org.linphone.databinding.ContactListCellBinding -import org.linphone.databinding.GenericListHeaderBinding -import org.linphone.utils.AppUtils -import org.linphone.utils.Event -import org.linphone.utils.HeaderAdapter - -class ContactsListAdapter( - selectionVM: ListTopBarViewModel, - private val viewLifecycleOwner: LifecycleOwner -) : SelectionListAdapter( - selectionVM, - ContactDiffCallback() -), - HeaderAdapter { - val selectedContactEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val binding: ContactListCellBinding = DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.contact_list_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: ContactListCellBinding - ) : RecyclerView.ViewHolder(binding.root) { - fun bind(contactViewModel: ContactViewModel) { - with(binding) { - viewModel = contactViewModel - - 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 { - val friend = contactViewModel.contact.value - // TODO FIXME !!! - if (friend != null) selectedContactEvent.value = Event(friend) - } - } - - setLongClickListener { - if (selectionViewModel.isEditionEnabled.value == false) { - selectionViewModel.isEditionEnabled.value = true - // Selection will be handled by click listener - true - } - false - } - - executePendingBindings() - } - } - } - - override fun displayHeaderForPosition(position: Int): Boolean { - if (position >= itemCount) return false - val contact = getItem(position) - val firstLetter = contact.fullName.firstOrNull().toString() - val previousPosition = position - 1 - return if (previousPosition >= 0) { - val previousItemFirstLetter = getItem(previousPosition).fullName.firstOrNull().toString() - !firstLetter.equals(previousItemFirstLetter, ignoreCase = true) - } else { - true - } - } - - override fun getHeaderViewForPosition(context: Context, position: Int): View { - val contact = getItem(position) - val firstLetter = AppUtils.getInitials(contact.fullName, 1) - val binding: GenericListHeaderBinding = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.generic_list_header, - null, - false - ) - binding.title = firstLetter - binding.executePendingBindings() - return binding.root - } -} - -private class ContactDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: ContactViewModel, - newItem: ContactViewModel - ): Boolean { - return oldItem.fullName.compareTo(newItem.fullName) == 0 - } - - override fun areContentsTheSame( - oldItem: ContactViewModel, - newItem: ContactViewModel - ): Boolean { - return true - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/adapters/SyncAccountAdapter.kt b/app/src/main/java/org/linphone/activities/main/contact/adapters/SyncAccountAdapter.kt deleted file mode 100644 index f8303bcb7..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/adapters/SyncAccountAdapter.kt +++ /dev/null @@ -1,49 +0,0 @@ -package org.linphone.activities.main.contact.adapters - -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter -import android.widget.ImageView -import android.widget.TextView -import kotlin.collections.ArrayList -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R - -class SyncAccountAdapter : BaseAdapter() { - private var accounts: ArrayList> = arrayListOf() - - init { - accounts.addAll(coreContext.contactsManager.getAvailableSyncAccounts()) - } - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view: View = convertView ?: LayoutInflater.from(parent.context).inflate( - R.layout.contact_sync_account_picker_cell, - parent, - false - ) - val account = getItem(position) - - val icon = view.findViewById(R.id.account_icon) - icon.setImageDrawable(account.third) - icon.contentDescription = account.second - val name = view.findViewById(R.id.account_name) - name.text = account.first - - return view - } - - override fun getItem(position: Int): Triple { - return accounts[position] - } - - override fun getItemId(position: Int): Long { - return position.toLong() - } - - override fun getCount(): Int { - return accounts.size - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/data/ContactEditorData.kt b/app/src/main/java/org/linphone/activities/main/contact/data/ContactEditorData.kt deleted file mode 100644 index 2b545fd57..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/data/ContactEditorData.kt +++ /dev/null @@ -1,360 +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 . - */ -package org.linphone.activities.main.contact.data - -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.media.ExifInterface -import android.provider.ContactsContract -import androidx.lifecycle.MutableLiveData -import java.io.ByteArrayOutputStream -import java.io.IOException -import kotlinx.coroutines.CoroutineScope -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.contact.* -import org.linphone.core.ChatRoom.SecurityLevel -import org.linphone.core.ConsolidatedPresence -import org.linphone.core.Friend -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils -import org.linphone.utils.ImageUtils -import org.linphone.utils.PermissionHelper - -class ContactEditorData(val friend: Friend?) : ContactDataInterface { - override val contact: MutableLiveData = MutableLiveData() - override val displayName: MutableLiveData = MutableLiveData() - override val securityLevel: MutableLiveData = MutableLiveData() - override val presenceStatus: MutableLiveData = MutableLiveData() - override val coroutineScope: CoroutineScope = coreContext.coroutineScope - - val firstName = MutableLiveData() - - val lastName = MutableLiveData() - - val organization = MutableLiveData() - - val displayOrganization = corePreferences.contactOrganizationVisible - - val tempPicturePath = MutableLiveData() - private var picture: ByteArray? = null - - val numbers = MutableLiveData>() - - val addresses = MutableLiveData>() - - var syncAccountName: String? = null - var syncAccountType: String? = null - - init { - if (friend != null) { - contact.value = friend!! - displayName.value = friend.name ?: "" - presenceStatus.value = friend.consolidatedPresence - friend.addListener { - presenceStatus.value = it.consolidatedPresence - } - } else { - displayName.value = "" - presenceStatus.value = ConsolidatedPresence.Offline - } - - organization.value = friend?.organization ?: "" - - firstName.value = "" - lastName.value = "" - val refKey = friend?.refKey - val vCard = friend?.vcard - if (vCard?.familyName.isNullOrEmpty() && vCard?.givenName.isNullOrEmpty()) { - if (refKey != null) { - Log.w("[Contact Editor] vCard first & last name not filled-in yet, doing it now") - fetchFirstAndLastNames(refKey) - } else { - Log.e( - "[Contact Editor] vCard first & last name not available as contact doesn't have a native ID" - ) - } - } else { - firstName.value = vCard?.givenName - lastName.value = vCard?.familyName - } - - updateNumbersAndAddresses(refKey) - } - - fun save(): Friend { - var contact = friend - var created = false - - if (contact == null) { - created = true - // From Crashlytics it seems both permissions are required... - val nativeId = if (PermissionHelper.get().hasReadContactsPermission() && - PermissionHelper.get().hasWriteContactsPermission() - ) { - Log.i("[Contact Editor] Creating native contact") - NativeContactEditor.createAndroidContact(syncAccountName, syncAccountType) - .toString() - } else { - Log.e("[Contact Editor] Can't create native contact, permission denied") - null - } - contact = coreContext.core.createFriend() - contact.refKey = nativeId - } - - if (contact.refKey != null) { - Log.i("[Contact Editor] Committing changes in native contact id ${contact.refKey}") - NativeContactEditor(contact) - .setFirstAndLastNames(firstName.value.orEmpty(), lastName.value.orEmpty()) - .setOrganization(organization.value.orEmpty()) - .setPhoneNumbers(numbers.value.orEmpty()) - .setSipAddresses(addresses.value.orEmpty()) - .setPicture(picture) - .commit() - } - - if (!created) contact.edit() - - contact.name = "${firstName.value.orEmpty()} ${lastName.value.orEmpty()}" - contact.organization = organization.value - - for (address in contact.addresses) { - contact.removeAddress(address) - } - for (address in addresses.value.orEmpty()) { - val sipAddress = address.newValue.value.orEmpty() - if (sipAddress.isEmpty() || address.toRemove.value == true) continue - - val parsed = coreContext.core.interpretUrl(sipAddress, false) - if (parsed != null) contact.addAddress(parsed) - } - - for (phone in contact.phoneNumbers) { - contact.removePhoneNumber(phone) - } - for (phone in numbers.value.orEmpty()) { - val phoneNumber = phone.newValue.value.orEmpty() - if (phoneNumber.isEmpty() || phone.toRemove.value == true) continue - - contact.addPhoneNumber(phoneNumber) - } - - val vCard = contact.vcard - if (vCard != null) { - vCard.familyName = lastName.value - vCard.givenName = firstName.value - } - - if (created) { - coreContext.core.defaultFriendList?.addLocalFriend(contact) - } else { - contact.done() - } - return contact - } - - fun setPictureFromPath(picturePath: String) { - var orientation = ExifInterface.ORIENTATION_NORMAL - var image = BitmapFactory.decodeFile(picturePath) - - try { - val ei = ExifInterface(picturePath) - orientation = ei.getAttributeInt( - ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_UNDEFINED - ) - Log.i("[Contact Editor] Exif rotation is $orientation") - } catch (e: IOException) { - Log.e("[Contact Editor] Failed to get Exif rotation, exception raised: $e") - } - - if (image == null) { - Log.e("[Contact Editor] Couldn't get bitmap from filePath: $picturePath") - return - } - - when (orientation) { - ExifInterface.ORIENTATION_ROTATE_90 -> - image = - ImageUtils.rotateImage(image, 90f) - ExifInterface.ORIENTATION_ROTATE_180 -> - image = - ImageUtils.rotateImage(image, 180f) - ExifInterface.ORIENTATION_ROTATE_270 -> - image = - ImageUtils.rotateImage(image, 270f) - } - - val stream = ByteArrayOutputStream() - image.compress(Bitmap.CompressFormat.JPEG, 100, stream) - picture = stream.toByteArray() - tempPicturePath.value = picturePath - image.recycle() - stream.close() - } - - fun addEmptySipAddress() { - val list = arrayListOf() - list.addAll(addresses.value.orEmpty()) - list.add(NumberOrAddressEditorData("", true)) - addresses.value = list - } - - fun addEmptyPhoneNumber() { - val list = arrayListOf() - list.addAll(numbers.value.orEmpty()) - list.add(NumberOrAddressEditorData("", false)) - numbers.value = list - } - - private fun updateNumbersAndAddresses(contactId: String?) { - val phoneNumbers = arrayListOf() - val sipAddresses = arrayListOf() - var fetched = false - - if (contactId != null) { - try { - // Try to get real values from contact to ensure edition/removal in native address book will go well - val cursor = coreContext.context.contentResolver.query( - ContactsContract.Data.CONTENT_URI, - arrayOf( - ContactsContract.Data.MIMETYPE, - ContactsContract.CommonDataKinds.Phone.NUMBER - ), - ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?", - arrayOf(contactId), - null - ) - - while (cursor != null && cursor.moveToNext()) { - val linphoneMime = AppUtils.getString(R.string.linphone_address_mime_type) - val mime: String? = - cursor.getString( - cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE) - ) - if (mime == ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE) { - val data1: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.Phone.NUMBER - ) - ) - if (data1 != null) { - phoneNumbers.add(NumberOrAddressEditorData(data1, false)) - } - } else if ( - mime == ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE || - mime == linphoneMime - ) { - val data1: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS - ) - ) - if (data1 != null) { - sipAddresses.add(NumberOrAddressEditorData(data1, true)) - } - } - } - - cursor?.close() - fetched = true - } catch (e: Exception) { - Log.e("[Contact Editor] Failed to sip addresses & phone number: $e") - fetched = false - } - } - - if (!fetched) { - Log.w( - "[Contact Editor] Fall-backing to friend info (might be inaccurate and thus edition/removal might fail)" - ) - for (number in friend?.phoneNumbers.orEmpty()) { - phoneNumbers.add(NumberOrAddressEditorData(number, false)) - } - - for (address in friend?.addresses.orEmpty()) { - sipAddresses.add(NumberOrAddressEditorData(address.asStringUriOnly(), true)) - } - } - - if (phoneNumbers.isEmpty()) { - phoneNumbers.add(NumberOrAddressEditorData("", false)) - } - numbers.value = phoneNumbers - - if (sipAddresses.isEmpty()) { - sipAddresses.add(NumberOrAddressEditorData("", true)) - } - addresses.value = sipAddresses - } - - private fun fetchFirstAndLastNames(contactId: String) { - try { - val cursor = coreContext.context.contentResolver.query( - ContactsContract.Data.CONTENT_URI, - arrayOf( - ContactsContract.Data.MIMETYPE, - ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME, - ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME - ), - ContactsContract.CommonDataKinds.StructuredName.CONTACT_ID + " = ?", - arrayOf(contactId), - null - ) - - while (cursor != null && cursor.moveToNext()) { - val mime: String? = cursor.getString( - cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE) - ) - if (mime == ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) { - val givenName: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.StructuredName.GIVEN_NAME - ) - ) - if (!givenName.isNullOrEmpty()) { - friend?.vcard?.givenName = givenName - firstName.value = givenName!! - } - - val familyName: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.StructuredName.FAMILY_NAME - ) - ) - if (!familyName.isNullOrEmpty()) { - friend?.vcard?.familyName = familyName - lastName.value = familyName!! - } - } - } - - cursor?.close() - } catch (e: Exception) { - Log.e("[Contact Editor] Failed to fetch first & last name: $e") - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/data/ContactNumberOrAddressData.kt b/app/src/main/java/org/linphone/activities/main/contact/data/ContactNumberOrAddressData.kt deleted file mode 100644 index 93f709d5e..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/data/ContactNumberOrAddressData.kt +++ /dev/null @@ -1,61 +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 . - */ -package org.linphone.activities.main.contact.data - -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.core.Address - -class ContactNumberOrAddressData( - val address: Address?, - val hasPresence: Boolean, - val displayedValue: String, - val isSip: Boolean = true, - val showSecureChat: Boolean = false, - val typeLabel: String = "", - private val listener: ContactNumberOrAddressClickListener -) { - val showInvite = !hasPresence && !isSip && corePreferences.showContactInviteBySms - - val chatAllowed = !corePreferences.disableChat - - val hidePlainChat = corePreferences.forceEndToEndEncryptedChat - - fun startCall() { - address ?: return - listener.onCall(address) - } - - fun startChat(secured: Boolean) { - address ?: return - listener.onChat(address, secured) - } - - fun smsInvite() { - listener.onSmsInvite(displayedValue) - } -} - -interface ContactNumberOrAddressClickListener { - fun onCall(address: Address) - - fun onChat(address: Address, isSecured: Boolean) - - fun onSmsInvite(number: String) -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/data/NumberOrAddressEditorData.kt b/app/src/main/java/org/linphone/activities/main/contact/data/NumberOrAddressEditorData.kt deleted file mode 100644 index 44c22cb39..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/data/NumberOrAddressEditorData.kt +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.main.contact.data - -import androidx.lifecycle.MutableLiveData - -class NumberOrAddressEditorData(val currentValue: String, val isSipAddress: Boolean) { - val newValue = MutableLiveData() - - val toRemove = MutableLiveData() - - init { - newValue.value = currentValue - toRemove.value = false - } - - fun remove() { - toRemove.value = true - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt deleted file mode 100644 index 31c5497ac..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/ContactEditorFragment.kt +++ /dev/null @@ -1,251 +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 . - */ -package org.linphone.activities.main.contact.fragments - -import android.app.Activity -import android.app.Dialog -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Bundle -import android.os.Parcelable -import android.provider.MediaStore -import android.view.View -import androidx.core.content.FileProvider -import androidx.lifecycle.lifecycleScope -import java.io.File -import kotlinx.coroutines.launch -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.main.MainActivity -import org.linphone.activities.main.contact.data.ContactEditorData -import org.linphone.activities.main.contact.data.NumberOrAddressEditorData -import org.linphone.activities.main.contact.viewmodels.* -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.navigateToContact -import org.linphone.core.tools.Log -import org.linphone.databinding.ContactEditorFragmentBinding -import org.linphone.utils.DialogUtils -import org.linphone.utils.FileUtils -import org.linphone.utils.PermissionHelper - -class ContactEditorFragment : GenericFragment(), SyncAccountPickerFragment.SyncAccountPickedListener { - private lateinit var data: ContactEditorData - private var temporaryPicturePath: File? = null - - override fun getLayoutId(): Int = R.layout.contact_editor_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - val contact = sharedViewModel.selectedContact.value - val contactRefKey = contact?.refKey - val friend = if (contactRefKey != null) coreContext.core.getFriendByRefKey(contactRefKey) else null - data = ContactEditorData(friend ?: contact) - binding.viewModel = data - - useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false - - binding.setAvatarClickListener { - pickFile() - } - - binding.setSaveChangesClickListener { - data.syncAccountName = null - data.syncAccountType = null - - if (data.friend == null) { - var atLeastASipAddressOrPhoneNumber = false - for (addr in data.addresses.value.orEmpty()) { - if (addr.newValue.value.orEmpty().isNotEmpty()) { - atLeastASipAddressOrPhoneNumber = true - break - } - } - if (!atLeastASipAddressOrPhoneNumber) { - for (number in data.numbers.value.orEmpty()) { - if (number.newValue.value.orEmpty().isNotEmpty()) { - atLeastASipAddressOrPhoneNumber = true - break - } - } - } - if (!atLeastASipAddressOrPhoneNumber) { - // Contact will be created without phone and SIP address - // Let's warn the user it won't be visible in Linphone app - Log.w( - "[Contact Editor] New contact without SIP address nor phone number, showing warning dialog" - ) - showInvisibleContactWarningDialog() - } else if (corePreferences.showNewContactAccountDialog) { - Log.i("[Contact Editor] New contact, ask user where to store it") - SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker") - } else { - Log.i("[Contact Editor] Saving new contact") - saveContact() - } - } else { - Log.i("[Contact Editor] Saving contact changes") - saveContact() - } - } - - val sipUri = arguments?.getString("SipUri") - if (sipUri != null) { - Log.i("[Contact Editor] Found SIP URI in arguments: $sipUri") - val newSipUri = NumberOrAddressEditorData("", true) - newSipUri.newValue.value = sipUri - - val list = arrayListOf() - list.addAll(data.addresses.value.orEmpty()) - list.add(newSipUri) - data.addresses.value = list - } - - if (!PermissionHelper.required(requireContext()).hasWriteContactsPermission()) { - Log.i("[Contact Editor] Asking for WRITE_CONTACTS permission") - requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 0) - } - } - - override fun onSyncAccountClicked(name: String?, type: String?) { - Log.i("[Contact Editor] Saving new contact using account $name / $type") - data.syncAccountName = name - data.syncAccountType = type - saveContact() - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == 0) { - val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED - if (granted) { - Log.i("[Contact Editor] WRITE_CONTACTS permission granted") - } else { - Log.w("[Contact Editor] WRITE_CONTACTS permission denied") - (activity as MainActivity).showSnackBar( - R.string.contact_editor_write_permission_denied - ) - goBack() - } - } - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { - if (resultCode == Activity.RESULT_OK) { - lifecycleScope.launch { - val contactImageFilePath = FileUtils.getFilePathFromPickerIntent( - intent, - temporaryPicturePath - ) - if (contactImageFilePath != null) { - data.setPictureFromPath(contactImageFilePath) - } - } - } - } - - private fun saveContact() { - val savedContact = data.save() - val id = savedContact.refKey - if (id != null) { - Log.i("[Contact Editor] Displaying contact $savedContact") - navigateToContact(id) - } else { - Log.w( - "[Contact Editor] Can't display $savedContact because it doesn't have a refKey, going back" - ) - goBack() - } - } - - private fun pickFile() { - val cameraIntents = ArrayList() - - // Handles image picking - val galleryIntent = Intent(Intent.ACTION_PICK) - galleryIntent.type = "image/*" - - if (PermissionHelper.get().hasCameraPermission()) { - // Allows to capture directly from the camera - val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - val tempFileName = System.currentTimeMillis().toString() + ".jpeg" - val file = FileUtils.getFileStoragePath(tempFileName) - temporaryPicturePath = file - val publicUri = FileProvider.getUriForFile( - requireContext(), - requireContext().getString(R.string.file_provider), - file - ) - captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, publicUri) - captureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - cameraIntents.add(captureIntent) - } - - val chooserIntent = - Intent.createChooser(galleryIntent, getString(R.string.chat_message_pick_file_dialog)) - chooserIntent.putExtra( - Intent.EXTRA_INITIAL_INTENTS, - cameraIntents.toArray(arrayOf()) - ) - - startActivityForResult(chooserIntent, 0) - } - - private fun showInvisibleContactWarningDialog() { - val dialogViewModel = - DialogViewModel(getString(R.string.contacts_new_contact_wont_be_visible_warning_dialog)) - val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) - - dialogViewModel.showCancelButton( - { - Log.i("[Contact Editor] Aborting new contact saving") - dialog.dismiss() - }, - getString(R.string.no) - ) - - dialogViewModel.showOkButton( - { - dialog.dismiss() - - if (corePreferences.showNewContactAccountDialog) { - Log.i("[Contact Editor] New contact, ask user where to store it") - SyncAccountPickerFragment(this).show(childFragmentManager, "SyncAccountPicker") - } else { - Log.i("[Contact Editor] Saving new contact") - saveContact() - } - }, - getString(R.string.yes) - ) - - dialog.show() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt deleted file mode 100644 index ab01a9b23..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/DetailContactFragment.kt +++ /dev/null @@ -1,194 +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 . - */ -package org.linphone.activities.main.contact.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.* -import org.linphone.activities.main.* -import org.linphone.activities.main.contact.viewmodels.ContactViewModel -import org.linphone.activities.main.contact.viewmodels.ContactViewModelFactory -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.navigateToChatRoom -import org.linphone.activities.navigateToContactEditor -import org.linphone.activities.navigateToDialer -import org.linphone.core.tools.Log -import org.linphone.databinding.ContactDetailFragmentBinding -import org.linphone.utils.DialogUtils -import org.linphone.utils.Event - -class DetailContactFragment : GenericFragment() { - private lateinit var viewModel: ContactViewModel - - override fun getLayoutId(): Int = R.layout.contact_detail_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - postponeEnterTransition() - - binding.lifecycleOwner = viewLifecycleOwner - - binding.sharedMainViewModel = sharedViewModel - - useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false - - val id = arguments?.getString("id") - arguments?.clear() - if (id != null) { - Log.i("[Contact] Found contact id parameter in arguments: $id") - sharedViewModel.selectedContact.value = coreContext.contactsManager.findContactById(id) - } - - val contact = sharedViewModel.selectedContact.value - if (contact == null) { - Log.e("[Contact] Friend is null, aborting!") - goBack() - return - } - - viewModel = ViewModelProvider( - this, - ContactViewModelFactory(contact) - )[ContactViewModel::class.java] - binding.viewModel = viewModel - - viewModel.sendSmsToEvent.observe( - viewLifecycleOwner - ) { - it.consume { number -> - sendSms(number) - } - } - - viewModel.startCallToEvent.observe( - viewLifecycleOwner - ) { - it.consume { address -> - if (coreContext.core.callsNb > 0) { - Log.i( - "[Contact] Starting dialer with pre-filled URI ${address.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}" - ) - sharedViewModel.updateContactsAnimationsBasedOnDestination.value = - Event(R.id.dialerFragment) - sharedViewModel.updateDialerAnimationsBasedOnDestination.value = - Event(R.id.masterContactsFragment) - - val args = Bundle() - args.putString("URI", address.asStringUriOnly()) - args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) - args.putBoolean( - "SkipAutoCallStart", - true - ) // If auto start call setting is enabled, ignore it - navigateToDialer(args) - } else { - coreContext.startCall(address) - } - } - } - - viewModel.chatRoomCreatedEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatRoom -> - sharedViewModel.updateContactsAnimationsBasedOnDestination.value = - Event(R.id.masterChatRoomsFragment) - val args = Bundle() - args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly()) - args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly()) - navigateToChatRoom(args) - } - } - - binding.setEditClickListener { - navigateToContactEditor() - } - - binding.setDeleteClickListener { - confirmContactRemoval() - } - - viewModel.onMessageToNotifyEvent.observe( - viewLifecycleOwner - ) { - it.consume { messageResourceId -> - (activity as MainActivity).showSnackBar(messageResourceId) - } - } - viewModel.updateNumbersAndAddresses() - - startPostponedEnterTransition() - } - - override fun onResume() { - super.onResume() - if (this::viewModel.isInitialized) { - viewModel.registerContactListener() - coreContext.contactsManager.contactIdToWatchFor = viewModel.contact.value?.refKey ?: "" - } - } - - override fun onPause() { - super.onPause() - coreContext.contactsManager.contactIdToWatchFor = "" - if (this::viewModel.isInitialized) { - viewModel.unregisterContactListener() - } - } - - private fun confirmContactRemoval() { - val dialogViewModel = DialogViewModel(getString(R.string.contact_delete_one_dialog)) - dialogViewModel.showIcon = true - dialogViewModel.iconResource = R.drawable.dialog_delete_icon - val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) - - dialogViewModel.showCancelButton { - dialog.dismiss() - } - - dialogViewModel.showDeleteButton( - { - viewModel.deleteContact() - dialog.dismiss() - goBack() - }, - getString(R.string.dialog_delete) - ) - - dialog.show() - } - - private fun sendSms(number: String) { - val smsIntent = Intent(Intent.ACTION_SENDTO) - smsIntent.putExtra("address", number) - smsIntent.data = Uri.parse("smsto:$number") - val text = getString(R.string.contact_send_sms_invite_text).format( - getString(R.string.contact_send_sms_invite_download_link) - ) - smsIntent.putExtra("sms_body", text) - startActivity(smsIntent) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt deleted file mode 100644 index d388c9e78..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/MasterContactsFragment.kt +++ /dev/null @@ -1,392 +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 . - */ -package org.linphone.activities.main.contact.fragments - -import android.app.Dialog -import android.content.pm.PackageManager -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 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.SnackBarActivity -import org.linphone.activities.clearDisplayedContact -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.contact.adapters.ContactsListAdapter -import org.linphone.activities.main.contact.viewmodels.ContactsListViewModel -import org.linphone.activities.main.fragments.MasterFragment -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.navigateToContact -import org.linphone.activities.navigateToContactEditor -import org.linphone.core.Factory -import org.linphone.core.Friend -import org.linphone.core.tools.Log -import org.linphone.databinding.ContactMasterFragmentBinding -import org.linphone.utils.* - -class MasterContactsFragment : MasterFragment() { - override val dialogConfirmationMessageBeforeRemoval = R.plurals.contact_delete_dialog - private lateinit var listViewModel: ContactsListViewModel - - private var sipUriToAdd: String? = null - private var editOnClick: Boolean = false - private var contactIdToDisplay: String? = null - - override fun getLayoutId(): Int = R.layout.contact_master_fragment - - override fun onDestroyView() { - binding.contactsList.adapter = null - super.onDestroyView() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - listViewModel = ViewModelProvider(this)[ContactsListViewModel::class.java] - binding.viewModel = listViewModel - - /* Shared view model & sliding pane related */ - - setUpSlidingPane(binding.slidingPane) - - useMaterialSharedAxisXForwardAnimation = false - sharedViewModel.updateContactsAnimationsBasedOnDestination.observe( - viewLifecycleOwner - ) { - it.consume { id -> - val forward = when (id) { - R.id.dialerFragment, R.id.masterChatRoomsFragment -> false - else -> true - } - if (corePreferences.enableAnimations) { - val portraitOrientation = - resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE - val axis = - if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y - enterTransition = MaterialSharedAxis(axis, forward) - reenterTransition = MaterialSharedAxis(axis, forward) - returnTransition = MaterialSharedAxis(axis, !forward) - exitTransition = MaterialSharedAxis(axis, !forward) - } - } - } - - sharedViewModel.layoutChangedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable - if (binding.slidingPane.isSlideable) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.contacts_nav_container) as NavHostFragment - if (navHostFragment.navController.currentDestination?.id == R.id.emptyContactFragment) { - Log.i( - "[Contacts] Foldable device has been folded, closing side pane with empty fragment" - ) - binding.slidingPane.closePane() - } - } - } - } - - /* End of shared view model & sliding pane related */ - - _adapter = ContactsListAdapter(listSelectionViewModel, viewLifecycleOwner) - binding.contactsList.setHasFixedSize(true) - binding.contactsList.adapter = adapter - - binding.setEditClickListener { - if (PermissionHelper.get().hasWriteContactsPermission()) { - listSelectionViewModel.isEditionEnabled.value = true - } else { - Log.i("[Contacts] Asking for WRITE_CONTACTS permission") - requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 1) - } - } - - val layoutManager = LinearLayoutManager(requireContext()) - binding.contactsList.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) - ) - val swipeListener = object : RecyclerViewSwipeListener { - override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {} - - override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) { - val viewModel = DialogViewModel(getString(R.string.contact_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("[Contacts] Index is out of bound, can't delete contact") - } else { - val contactViewModel = adapter.currentList[index] - if (contactViewModel.isNativeContact.value == false) { - adapter.notifyItemChanged(index) - val activity = requireActivity() as MainActivity - activity.showSnackBar(R.string.contact_cant_be_deleted) - return - } - - viewModel.showCancelButton { - adapter.notifyItemChanged(index) - dialog.dismiss() - } - - viewModel.showDeleteButton( - { - val deletedContact = - adapter.currentList[index].contact.value - if (deletedContact != null) { - listViewModel.deleteContact(deletedContact) - if (!binding.slidingPane.isSlideable && - deletedContact == sharedViewModel.selectedContact.value - ) { - Log.i( - "[Contacts] Currently displayed contact has been deleted, removing detail fragment" - ) - clearDisplayedContact() - } - } - dialog.dismiss() - }, - getString(R.string.dialog_delete) - ) - } - - dialog.show() - } - } - - if (!corePreferences.readOnlyNativeContacts) { - RecyclerViewSwipeUtils(ItemTouchHelper.LEFT, swipeConfiguration, swipeListener) - .attachToRecyclerView(binding.contactsList) - } - - // Divider between items - binding.contactsList.addItemDecoration( - AppUtils.getDividerDecoration(requireContext(), layoutManager) - ) - - // Displays the first letter header - val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) - binding.contactsList.addItemDecoration(headerItemDecoration) - - adapter.selectedContactEvent.observe( - viewLifecycleOwner - ) { - it.consume { contact -> - Log.d("[Contacts] Selected item in list changed: $contact") - sharedViewModel.selectedContact.value = contact - (requireActivity() as MainActivity).hideKeyboard() - - if (editOnClick) { - navigateToContactEditor(sipUriToAdd, binding.slidingPane) - editOnClick = false - sipUriToAdd = null - } else { - navigateToContact() - binding.slidingPane.openPane() - } - } - } - - coreContext.contactsManager.fetchInProgress.observe( - viewLifecycleOwner - ) { - listViewModel.fetchInProgress.value = it - } - - listViewModel.contactsList.observe( - viewLifecycleOwner - ) { - val id = contactIdToDisplay - if (id != null) { - val contact = coreContext.contactsManager.findContactById(id) - if (contact != null) { - contactIdToDisplay = null - Log.i("[Contacts] Found matching contact [$contact] after callback") - adapter.selectedContactEvent.value = Event(contact) - } else { - Log.w("[Contacts] No contact found matching id [$id] after callback") - } - } - adapter.submitList(it) - } - - listViewModel.moreResultsAvailableEvent.observe( - viewLifecycleOwner - ) { - it.consume { - (requireActivity() as SnackBarActivity).showSnackBar( - R.string.contacts_ldap_query_more_results_available - ) - } - } - - binding.setAllContactsToggleClickListener { - listViewModel.sipContactsSelected.value = false - } - binding.setSipContactsToggleClickListener { - listViewModel.sipContactsSelected.value = true - } - - listViewModel.sipContactsSelected.observe( - viewLifecycleOwner - ) { - listViewModel.updateContactsList(true) - } - - listViewModel.filter.observe( - viewLifecycleOwner - ) { - listViewModel.updateContactsList(false) - } - - binding.setNewContactClickListener { - // Remove any previously selected contact - sharedViewModel.selectedContact.value = null - editOnClick = false - navigateToContactEditor(sipUriToAdd, binding.slidingPane) - sipUriToAdd = null - } - - val id = arguments?.getString("id") - val sipUri = arguments?.getString("sipUri") - val addressString = arguments?.getString("address") - arguments?.clear() - - if (id != null) { - Log.i("[Contacts] Found contact id parameter in arguments [$id]") - val contact = coreContext.contactsManager.findContactById(id) - if (contact != null) { - Log.i("[Contacts] Found matching contact [${contact.name}]") - adapter.selectedContactEvent.value = Event(contact) - } else { - Log.w( - "[Contacts] Matching contact not found yet, waiting for contacts updated callback" - ) - contactIdToDisplay = id - } - } else if (sipUri != null) { - Log.i("[Contacts] Found sipUri parameter in arguments [$sipUri]") - sipUriToAdd = sipUri - (activity as MainActivity).showSnackBar( - R.string.contact_choose_existing_or_new_to_add_number - ) - editOnClick = true - } else if (addressString != null) { - val address = Factory.instance().createAddress(addressString) - if (address != null) { - Log.i( - "[Contacts] Found friend SIP address parameter in arguments [${address.asStringUriOnly()}]" - ) - val contact = coreContext.contactsManager.findContactByAddress(address) - if (contact != null) { - Log.i("[Contacts] Found matching contact $contact") - adapter.selectedContactEvent.value = Event(contact) - } else { - Log.w( - "[Contacts] No matching contact found for SIP address [${address.asStringUriOnly()}]" - ) - } - } - } - - if (corePreferences.enableNativeAddressBookIntegration) { - if (!PermissionHelper.get().hasReadContactsPermission()) { - Log.i("[Contacts] Asking for READ_CONTACTS permission") - requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0) - } - } - } - - override fun deleteItems(indexesOfItemToDelete: ArrayList) { - val list = ArrayList() - var closeSlidingPane = false - for (index in indexesOfItemToDelete) { - val contact = adapter.currentList[index].contact.value - if (contact != null) { - list.add(contact) - } - - if (contact == sharedViewModel.selectedContact.value) { - closeSlidingPane = true - } - } - listViewModel.deleteContacts(list) - - if (!binding.slidingPane.isSlideable && closeSlidingPane) { - Log.i( - "[Contacts] Currently displayed contact has been deleted, removing detail fragment" - ) - clearDisplayedContact() - } - } - - override fun onResume() { - super.onResume() - listViewModel.updateContactsList(true) - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == 0) { - val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED - if (granted) { - Log.i("[Contacts] READ_CONTACTS permission granted") - coreContext.fetchContacts() - } else { - Log.w("[Contacts] READ_CONTACTS permission denied") - } - } else if (requestCode == 1) { - val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED - if (granted) { - Log.i("[Contacts] WRITE_CONTACTS permission granted") - listSelectionViewModel.isEditionEnabled.value = true - } else { - Log.w("[Contacts] WRITE_CONTACTS permission denied") - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/fragments/SyncAccountPickerFragment.kt b/app/src/main/java/org/linphone/activities/main/contact/fragments/SyncAccountPickerFragment.kt deleted file mode 100644 index 64e31bb05..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/fragments/SyncAccountPickerFragment.kt +++ /dev/null @@ -1,76 +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 . - */ -package org.linphone.activities.main.contact.fragments - -import android.os.Bundle -import android.view.* -import androidx.fragment.app.DialogFragment -import org.linphone.R -import org.linphone.activities.main.contact.adapters.SyncAccountAdapter -import org.linphone.core.tools.Log -import org.linphone.databinding.ContactSyncAccountPickerFragmentBinding - -class SyncAccountPickerFragment(private val listener: SyncAccountPickedListener) : DialogFragment() { - private var _binding: ContactSyncAccountPickerFragmentBinding? = null - private val binding get() = _binding!! - private lateinit var adapter: SyncAccountAdapter - - 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 = ContactSyncAccountPickerFragmentBinding.inflate(inflater, container, false) - - adapter = SyncAccountAdapter() - binding.accountsList.adapter = adapter - - binding.accountsList.setOnItemClickListener { _, _, position, _ -> - if (position >= 0 && position < adapter.count) { - val account = adapter.getItem(position) - Log.i("[Sync Account Picker] Picked ${account.first} / ${account.second}") - listener.onSyncAccountClicked(account.first, account.second) - } - dismiss() - } - - binding.setLocalSyncAccountClickListener { - Log.i("[Sync Account Picker] Picked local account") - listener.onSyncAccountClicked(null, null) - dismiss() - } - - return binding.root - } - - interface SyncAccountPickedListener { - fun onSyncAccountClicked(name: String?, type: String?) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt deleted file mode 100644 index 68ac85ace..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.contact.viewmodels - -import android.content.ContentProviderOperation -import android.provider.ContactsContract -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.CoroutineScope -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.contact.data.ContactNumberOrAddressClickListener -import org.linphone.activities.main.contact.data.ContactNumberOrAddressData -import org.linphone.activities.main.viewmodels.MessageNotifierViewModel -import org.linphone.contact.ContactDataInterface -import org.linphone.contact.ContactsUpdatedListenerStub -import org.linphone.contact.hasLongTermPresence -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 ContactViewModelFactory(private val friend: Friend) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return ContactViewModel(friend) as T - } -} - -class ContactViewModel(friend: Friend) : MessageNotifierViewModel(), ContactDataInterface { - override val contact: MutableLiveData = MutableLiveData() - override val displayName: MutableLiveData = MutableLiveData() - override val securityLevel: MutableLiveData = MutableLiveData() - override val presenceStatus: MutableLiveData = MutableLiveData() - override val coroutineScope: CoroutineScope = viewModelScope - - var fullName = "" - - val displayOrganization = corePreferences.displayOrganization - - val numbersAndAddresses = MutableLiveData>() - - val sendSmsToEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val startCallToEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val chatRoomCreatedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val waitForChatRoomCreation = MutableLiveData() - - val isNativeContact = MutableLiveData() - - val readOnlyNativeAddressBook = MutableLiveData() - - val hasLongTermPresence = MutableLiveData() - - private val chatRoomListener = object : ChatRoomListenerStub() { - override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { - if (state == ChatRoom.State.Created) { - chatRoom.removeListener(this) - waitForChatRoomCreation.value = false - chatRoomCreatedEvent.value = Event(chatRoom) - } else if (state == ChatRoom.State.CreationFailed) { - Log.e("[Contact Detail] Group chat room creation has failed !") - chatRoom.removeListener(this) - waitForChatRoomCreation.value = false - onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) - } - } - } - - private val contactsListener = object : ContactsUpdatedListenerStub() { - override fun onContactUpdated(friend: Friend) { - if (friend.refKey == contact.value?.refKey) { - Log.i("[Contact Detail] Friend has been updated!") - contact.value = friend - displayName.value = friend.name - isNativeContact.value = friend.refKey != null - updateNumbersAndAddresses() - } - } - } - - private val listener = object : ContactNumberOrAddressClickListener { - override fun onCall(address: Address) { - startCallToEvent.value = Event(address) - } - - override fun onChat(address: Address, isSecured: Boolean) { - waitForChatRoomCreation.value = true - val chatRoom = LinphoneUtils.createOneToOneChatRoom(address, isSecured) - - if (chatRoom != null) { - val state = chatRoom.state - Log.i("[Contact Detail] Found existing chat room in state $state") - if (state == ChatRoom.State.Created || state == ChatRoom.State.Terminated) { - waitForChatRoomCreation.value = false - chatRoomCreatedEvent.value = Event(chatRoom) - } else { - chatRoom.addListener(chatRoomListener) - } - } else { - waitForChatRoomCreation.value = false - Log.e("[Contact Detail] Couldn't create chat room with address $address") - onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) - } - } - - override fun onSmsInvite(number: String) { - sendSmsToEvent.value = Event(number) - } - } - - init { - fullName = friend.name ?: "" - - contact.value = friend - displayName.value = friend.name - isNativeContact.value = friend.refKey != null - presenceStatus.value = friend.consolidatedPresence - readOnlyNativeAddressBook.value = corePreferences.readOnlyNativeContacts - hasLongTermPresence.value = friend.hasLongTermPresence() - - friend.addListener { - presenceStatus.value = it.consolidatedPresence - hasLongTermPresence.value = it.hasLongTermPresence() - } - } - - override fun onCleared() { - destroy() - super.onCleared() - } - - fun destroy() { - } - - fun registerContactListener() { - coreContext.contactsManager.addListener(contactsListener) - } - - fun unregisterContactListener() { - coreContext.contactsManager.removeListener(contactsListener) - } - - fun deleteContact() { - val select = ContactsContract.Data.CONTACT_ID + " = ?" - val ops = java.util.ArrayList() - - val id = contact.value?.refKey - if (id != null) { - Log.i("[Contact] Setting Android contact id $id to batch removal") - val args = arrayOf(id) - ops.add( - ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI) - .withSelection(select, args) - .build() - ) - } - - contact.value?.remove() - - if (ops.isNotEmpty()) { - try { - Log.i("[Contact] Removing ${ops.size} contacts") - coreContext.context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) - } catch (e: Exception) { - Log.e("[Contact] $e") - } - } - } - - fun updateNumbersAndAddresses() { - val list = arrayListOf() - val friend = contact.value ?: return - - for (address in friend.addresses) { - val username = address.username - if (username in friend.phoneNumbers) continue - - val value = address.asStringUriOnly() - val presenceModel = friend.getPresenceModelForUriOrTel(value) - val hasPresence = presenceModel?.basicStatus == PresenceBasicStatus.Open - val isMe = coreContext.core.defaultAccount?.params?.identityAddress?.weakEqual(address) ?: false - val hasLimeCapability = corePreferences.allowEndToEndEncryptedChatWithoutPresence || ( - friend.getPresenceModelForUriOrTel( - value - )?.hasCapability(Friend.Capability.LimeX3Dh) ?: false - ) - val secureChatAllowed = LinphoneUtils.isEndToEndEncryptedChatAvailable() && !isMe && hasLimeCapability - val displayValue = if (coreContext.core.defaultAccount?.params?.domain == address.domain) (address.username ?: value) else value - val noa = ContactNumberOrAddressData( - address, - hasPresence, - displayValue, - showSecureChat = secureChatAllowed, - listener = listener - ) - list.add(noa) - } - - for (phoneNumber in friend.phoneNumbersWithLabel) { - val number = phoneNumber.phoneNumber - val presenceModel = friend.getPresenceModelForUriOrTel(number) - val hasPresence = presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open - val contactAddress = presenceModel?.contact ?: number - val address = coreContext.core.interpretUrl( - contactAddress, - LinphoneUtils.applyInternationalPrefix() - ) - address?.displayName = displayName.value.orEmpty() - val isMe = if (address != null) { - coreContext.core.defaultAccount?.params?.identityAddress?.weakEqual( - address - ) ?: false - } else { - false - } - val hasLimeCapability = corePreferences.allowEndToEndEncryptedChatWithoutPresence || ( - friend.getPresenceModelForUriOrTel( - number - )?.hasCapability(Friend.Capability.LimeX3Dh) ?: false - ) - val secureChatAllowed = LinphoneUtils.isEndToEndEncryptedChatAvailable() && !isMe && hasLimeCapability - val label = PhoneNumberUtils.vcardParamStringToAddressBookLabel( - coreContext.context.resources, - phoneNumber.label ?: "" - ) - val noa = ContactNumberOrAddressData( - address, - hasPresence, - number, - isSip = false, - showSecureChat = secureChatAllowed, - typeLabel = label, - listener = listener - ) - list.add(noa) - } - numbersAndAddresses.postValue(list) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt deleted file mode 100644 index 7ab26ee8e..000000000 --- a/app/src/main/java/org/linphone/activities/main/contact/viewmodels/ContactsListViewModel.kt +++ /dev/null @@ -1,232 +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 . - */ -package org.linphone.activities.main.contact.viewmodels - -import android.content.ContentProviderOperation -import android.provider.ContactsContract -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import java.util.* -import kotlinx.coroutines.* -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.contact.ContactsUpdatedListenerStub -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.Event - -class ContactsListViewModel : ViewModel() { - val sipContactsSelected = MutableLiveData() - - val contactsList = MutableLiveData>() - - val nativeAddressBookEnabled = MutableLiveData() - - val readOnlyNativeAddressBook = MutableLiveData() - - val hideSipContactsList = MutableLiveData() - - val onlyShowSipContactsList = MutableLiveData() - - val fetchInProgress = MutableLiveData() - private var searchResultsPending: Boolean = false - private var fastFetchJob: Job? = null - - val filter = MutableLiveData() - private var previousFilter = "NotSet" - - val moreResultsAvailableEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { - override fun onContactsUpdated() { - Log.i("[Contacts] Contacts have changed") - updateContactsList(true) - } - } - - private val magicSearchListener = object : MagicSearchListenerStub() { - override fun onSearchResultsReceived(magicSearch: MagicSearch) { - Log.i("[Contacts] Magic search contacts available") - searchResultsPending = false - processMagicSearchResults(magicSearch.lastSearch) - // Use coreContext.contactsManager.fetchInProgress instead of false in case contacts are still being loaded - fetchInProgress.value = coreContext.contactsManager.fetchInProgress.value - } - - override fun onLdapHaveMoreResults(magicSearch: MagicSearch, ldap: Ldap) { - moreResultsAvailableEvent.value = Event(true) - } - } - - init { - sipContactsSelected.value = coreContext.contactsManager.shouldDisplaySipContactsList() - nativeAddressBookEnabled.value = corePreferences.enableNativeAddressBookIntegration - readOnlyNativeAddressBook.value = corePreferences.readOnlyNativeContacts - - onlyShowSipContactsList.value = corePreferences.onlyShowSipContactsList - hideSipContactsList.value = corePreferences.hideSipContactsList - if (onlyShowSipContactsList.value == true) { - sipContactsSelected.value = true - } - if (hideSipContactsList.value == true) { - sipContactsSelected.value = false - } - - coreContext.contactsManager.addListener(contactsUpdatedListener) - coreContext.contactsManager.magicSearch.addListener(magicSearchListener) - } - - override fun onCleared() { - contactsList.value.orEmpty().forEach(ContactViewModel::destroy) - coreContext.contactsManager.magicSearch.removeListener(magicSearchListener) - coreContext.contactsManager.removeListener(contactsUpdatedListener) - - super.onCleared() - } - - fun updateContactsList(clearCache: Boolean) { - val filterValue = filter.value.orEmpty() - - if (clearCache || ( - previousFilter.isNotEmpty() && ( - previousFilter.length > filterValue.length || - (previousFilter.length == filterValue.length && previousFilter != filterValue) - ) - ) - ) { - coreContext.contactsManager.magicSearch.resetSearchCache() - } - previousFilter = filterValue - - val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else "" - val sources = MagicSearch.Source.Friends.toInt() or MagicSearch.Source.LdapServers.toInt() - val aggregation = MagicSearch.Aggregation.Friend - searchResultsPending = true - fastFetchJob?.cancel() - Log.i( - "[Contacts] Asking Magic search for contacts matching filter [$filterValue], domain [$domain] and in sources [$sources]" - ) - coreContext.contactsManager.magicSearch.getContactsListAsync( - filterValue, - domain, - sources, - aggregation - ) - - val spinnerDelay = corePreferences.delayBeforeShowingContactsSearchSpinner.toLong() - fastFetchJob = viewModelScope.launch { - withContext(Dispatchers.IO) { - delay(spinnerDelay) - } - withContext(Dispatchers.Main) { - if (searchResultsPending) { - fetchInProgress.value = true - } - } - } - } - - private fun processMagicSearchResults(results: Array) { - Log.i("[Contacts] Processing ${results.size} results") - contactsList.value.orEmpty().forEach(ContactViewModel::destroy) - - val list = arrayListOf() - - for (result in results) { - val friend = result.friend - - val viewModel = if (friend != null) { - ContactViewModel(friend) - } else { - Log.w("[Contacts] SearchResult [$result] has no Friend!") - val fakeFriend = coreContext.contactsManager.createFriendFromSearchResult( - result - ) - ContactViewModel(fakeFriend) - } - - list.add(viewModel) - } - - contactsList.value = list - Log.i("[Contacts] Processed ${results.size} results") - } - - fun deleteContact(friend: Friend) { - friend.remove() // TODO: FIXME: friend is const here! - - val id = friend.refKey - if (id == null) { - Log.w("[Contacts] Friend has no refkey, can't delete it from native address book") - return - } - - val select = ContactsContract.Data.CONTACT_ID + " = ?" - val ops = ArrayList() - - Log.i("[Contacts] Adding Android contact id $id to batch removal") - val args = arrayOf(id) - ops.add( - ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI) - .withSelection(select, args) - .build() - ) - - if (ops.isNotEmpty()) { - try { - Log.i("[Contacts] Removing ${ops.size} contacts") - coreContext.context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) - } catch (e: Exception) { - Log.e("[Contacts] $e") - } - } - } - - fun deleteContacts(list: ArrayList) { - val select = ContactsContract.Data.CONTACT_ID + " = ?" - val ops = ArrayList() - - for (friend in list) { - val id = friend.refKey - if (id != null) { - Log.i("[Contacts] Adding Android contact id $id to batch removal") - val args = arrayOf(id) - ops.add( - ContentProviderOperation.newDelete(ContactsContract.RawContacts.CONTENT_URI) - .withSelection(select, args) - .build() - ) - } - friend.remove() - } - - if (ops.isNotEmpty()) { - try { - Log.i("[Contacts] Removing ${ops.size} contacts") - coreContext.context.contentResolver.applyBatch(ContactsContract.AUTHORITY, ops) - } catch (e: Exception) { - Log.e("[Contacts] $e") - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/dialer/NumpadDigitListener.kt b/app/src/main/java/org/linphone/activities/main/dialer/NumpadDigitListener.kt deleted file mode 100644 index 49cfa6437..000000000 --- a/app/src/main/java/org/linphone/activities/main/dialer/NumpadDigitListener.kt +++ /dev/null @@ -1,25 +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 . - */ -package org.linphone.activities.main.dialer - -interface NumpadDigitListener { - fun handleClick(key: Char) - fun handleLongClick(key: Char): Boolean -} diff --git a/app/src/main/java/org/linphone/activities/main/dialer/fragments/ConfigViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/dialer/fragments/ConfigViewerFragment.kt deleted file mode 100644 index 9ec0e6d47..000000000 --- a/app/src/main/java/org/linphone/activities/main/dialer/fragments/ConfigViewerFragment.kt +++ /dev/null @@ -1,64 +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 . - */ -package org.linphone.activities.main.dialer.fragments - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModelProvider -import org.linphone.R -import org.linphone.activities.main.dialer.viewmodels.ConfigFileViewModel -import org.linphone.activities.main.fragments.SecureFragment -import org.linphone.core.tools.Log -import org.linphone.databinding.FileConfigViewerFragmentBinding - -class ConfigViewerFragment : SecureFragment() { - private lateinit var viewModel: ConfigFileViewModel - - override fun getLayoutId(): Int = R.layout.file_config_viewer_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - viewModel = ViewModelProvider(this)[ConfigFileViewModel::class.java] - binding.viewModel = viewModel - - isSecure = arguments?.getBoolean("Secure") ?: false - - binding.setExportClickListener { - shareConfig() - } - } - - private fun shareConfig() { - val intent = Intent(Intent.ACTION_SEND) - intent.putExtra(Intent.EXTRA_TEXT, viewModel.text.value.orEmpty()) - intent.type = "text/plain" - - try { - startActivity(Intent.createChooser(intent, getString(R.string.app_name))) - } catch (ex: ActivityNotFoundException) { - Log.e(ex) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt b/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt deleted file mode 100644 index 926442817..000000000 --- a/app/src/main/java/org/linphone/activities/main/dialer/fragments/DialerFragment.kt +++ /dev/null @@ -1,401 +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 . - */ -package org.linphone.activities.main.dialer.fragments - -import android.Manifest -import android.annotation.TargetApi -import android.app.Dialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.Configuration -import android.net.Uri -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModelProvider -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.transition.MaterialSharedAxis -import org.linphone.BuildConfig -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.dialer.viewmodels.DialerViewModel -import org.linphone.activities.main.fragments.SecureFragment -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.navigateToConferenceScheduling -import org.linphone.activities.navigateToConfigFileViewer -import org.linphone.activities.navigateToContacts -import org.linphone.compatibility.Compatibility -import org.linphone.core.tools.Log -import org.linphone.databinding.DialerFragmentBinding -import org.linphone.mediastream.Version -import org.linphone.telecom.TelecomHelper -import org.linphone.utils.AppUtils -import org.linphone.utils.DialogUtils -import org.linphone.utils.Event -import org.linphone.utils.PermissionHelper - -class DialerFragment : SecureFragment() { - private lateinit var viewModel: DialerViewModel - - private var uploadLogsInitiatedByUs = false - - override fun getLayoutId(): Int = R.layout.dialer_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - viewModel = ViewModelProvider(this)[DialerViewModel::class.java] - binding.viewModel = viewModel - - useMaterialSharedAxisXForwardAnimation = false - sharedViewModel.updateDialerAnimationsBasedOnDestination.observe( - viewLifecycleOwner - ) { - it.consume { id -> - val forward = when (id) { - R.id.masterChatRoomsFragment -> false - else -> true - } - if (corePreferences.enableAnimations) { - val portraitOrientation = - resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE - val axis = - if (portraitOrientation) MaterialSharedAxis.X else MaterialSharedAxis.Y - enterTransition = MaterialSharedAxis(axis, forward) - reenterTransition = MaterialSharedAxis(axis, forward) - returnTransition = MaterialSharedAxis(axis, !forward) - exitTransition = MaterialSharedAxis(axis, !forward) - } - } - } - - binding.setNewContactClickListener { - sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event( - R.id.masterContactsFragment - ) - sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event( - R.id.dialerFragment - ) - navigateToContacts(viewModel.enteredUri.value) - } - - binding.setNewConferenceClickListener { - sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event( - R.id.conferenceSchedulingFragment - ) - navigateToConferenceScheduling() - } - - binding.setTransferCallClickListener { - if (viewModel.transferCall()) { - // Transfer has been consumed, otherwise it might have been a "bis" use - sharedViewModel.pendingCallTransfer = false - viewModel.transferVisibility.value = false - coreContext.onCallStarted() - } - } - - viewModel.enteredUri.observe( - viewLifecycleOwner - ) { - if (it == corePreferences.debugPopupCode) { - displayDebugPopup() - viewModel.enteredUri.value = "" - } - } - - viewModel.uploadFinishedEvent.observe( - viewLifecycleOwner - ) { - it.consume { url -> - // To prevent being trigger when using the Send Logs button in About page - if (uploadLogsInitiatedByUs) { - val clipboard = - requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Logs url", url) - clipboard.setPrimaryClip(clip) - - val activity = requireActivity() as MainActivity - activity.showSnackBar(R.string.logs_url_copied_to_clipboard) - - AppUtils.shareUploadedLogsUrl(activity, url) - } - } - } - - viewModel.updateAvailableEvent.observe( - viewLifecycleOwner - ) { - it.consume { url -> - displayNewVersionAvailableDialog(url) - } - } - - viewModel.onMessageToNotifyEvent.observe( - viewLifecycleOwner - ) { - it.consume { resourceId -> - (requireActivity() as MainActivity).showSnackBar(resourceId) - } - } - - if (corePreferences.firstStart) { - Log.w( - "[Dialer] First start detected, wait for assistant to be finished to check for update & request permissions" - ) - return - } - - if (arguments?.containsKey("URI") == true) { - val address = arguments?.getString("URI") ?: "" - Log.i("[Dialer] Found URI to call: $address") - val skipAutoCall = arguments?.getBoolean("SkipAutoCallStart") ?: false - - if (corePreferences.skipDialerForNewCallAndTransfer) { - if (sharedViewModel.pendingCallTransfer) { - Log.i( - "[Dialer] We were asked to skip dialer so starting new call to [$address] now" - ) - viewModel.transferCallTo(address) - } else { - Log.i( - "[Dialer] We were asked to skip dialer so starting transfer to [$address] now" - ) - viewModel.directCall(address) - } - } else if (corePreferences.callRightAway && !skipAutoCall) { - Log.i("[Dialer] Call right away setting is enabled, start the call to [$address]") - viewModel.directCall(address) - } else { - sharedViewModel.dialerUri = address - } - } - arguments?.clear() - - Log.i("[Dialer] Pending call transfer mode = ${sharedViewModel.pendingCallTransfer}") - viewModel.transferVisibility.value = sharedViewModel.pendingCallTransfer - - viewModel.autoInitiateVideoCalls.value = coreContext.core.videoActivationPolicy.automaticallyInitiate - - checkForUpdate() - - checkPermissions() - } - - override fun onPause() { - sharedViewModel.dialerUri = viewModel.enteredUri.value ?: "" - super.onPause() - } - - override fun onResume() { - super.onResume() - - if ((requireActivity() as GenericActivity).isTablet()) { - coreContext.core.nativePreviewWindowId = binding.videoPreviewWindow - } - - viewModel.updateShowVideoPreview() - viewModel.autoInitiateVideoCalls.value = coreContext.core.videoActivationPolicy.automaticallyInitiate - uploadLogsInitiatedByUs = false - - viewModel.enteredUri.value = sharedViewModel.dialerUri - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == 0) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Dialer] READ_PHONE_STATE permission has been granted") - coreContext.initPhoneStateListener() - // If first permission has been granted, continue to ask for permissions, - // otherwise don't do it or it will loop indefinitely - checkPermissions() - } - } else if (requestCode == 1) { - var allGranted = true - for (result in grantResults) { - if (result != PackageManager.PERMISSION_GRANTED) { - allGranted = false - } - } - if (allGranted) { - Log.i("[Dialer] Telecom Manager permission have been granted") - enableTelecomManager() - } else { - Log.w("[Dialer] Telecom Manager permission have been denied (at least one of them)") - } - } else if (requestCode == 2) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Dialer] POST_NOTIFICATIONS permission has been granted") - } - checkTelecomManagerPermissions() - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - } - - private fun checkPermissions() { - if (!PermissionHelper.get().hasReadPhoneStatePermission()) { - Log.i("[Dialer] Asking for READ_PHONE_STATE permission") - requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), 0) - } else if (!PermissionHelper.get().hasPostNotificationsPermission()) { - // Don't check the following the previous permission is being asked - Log.i("[Dialer] Asking for POST_NOTIFICATIONS permission") - Compatibility.requestPostNotificationsPermission(this, 2) - } else if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - // Don't check the following the previous permissions are being asked - checkTelecomManagerPermissions() - } - - // See https://developer.android.com/about/versions/14/behavior-changes-14#fgs-types - if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { - val fullScreenIntentPermission = Compatibility.hasFullScreenIntentPermission( - requireContext() - ) - Log.i( - "[Dialer] Android 14 or above detected: full-screen intent permission is ${if (fullScreenIntentPermission) "granted" else "not granted"}" - ) - if (!fullScreenIntentPermission) { - (requireActivity() as MainActivity).showSnackBar( - R.string.android_14_full_screen_intent_permission_not_granted, - R.string.android_14_go_to_full_screen_intent_permission_setting - ) { - Compatibility.requestFullScreenIntentPermission(requireContext()) - } - } - } - } - - @TargetApi(Version.API26_O_80) - private fun checkTelecomManagerPermissions() { - if (!corePreferences.useTelecomManager) { - Log.i("[Dialer] Telecom Manager feature is disabled") - if (corePreferences.manuallyDisabledTelecomManager) { - Log.w("[Dialer] User has manually disabled Telecom Manager feature") - } else { - if (Compatibility.hasTelecomManagerPermissions(requireContext())) { - enableTelecomManager() - } else { - Log.i("[Dialer] Asking for Telecom Manager permissions") - Compatibility.requestTelecomManagerPermissions(requireActivity(), 1) - } - } - } else { - Log.i("[Dialer] Telecom Manager feature is already enabled") - } - } - - @TargetApi(Version.API26_O_80) - private fun enableTelecomManager() { - Log.i("[Dialer] Telecom Manager permissions granted") - if (!TelecomHelper.exists()) { - Log.i("[Dialer] Creating Telecom Helper") - if (Compatibility.hasTelecomManagerFeature(requireContext())) { - TelecomHelper.create(requireContext()) - } else { - Log.e( - "[Dialer] Telecom Helper can't be created, device doesn't support connection service!" - ) - return - } - } else { - Log.e("[Dialer] Telecom Manager was already created ?!") - } - corePreferences.useTelecomManager = true - } - - private fun displayDebugPopup() { - val alertDialog = MaterialAlertDialogBuilder(requireContext()) - alertDialog.setTitle(getString(R.string.debug_popup_title)) - - val items = if (corePreferences.debugLogs) { - resources.getStringArray(R.array.popup_send_log) - } else { - resources.getStringArray(R.array.popup_enable_log) - } - - alertDialog.setItems(items) { _, which -> - when (items[which]) { - getString(R.string.debug_popup_disable_logs) -> { - corePreferences.debugLogs = false - } - getString(R.string.debug_popup_enable_logs) -> { - corePreferences.debugLogs = true - } - getString(R.string.debug_popup_send_logs) -> { - uploadLogsInitiatedByUs = true - viewModel.uploadLogs() - } - getString(R.string.debug_popup_show_config_file) -> { - navigateToConfigFileViewer() - } - } - } - - alertDialog.show() - } - - private fun checkForUpdate() { - val lastTimestamp: Int = corePreferences.lastUpdateAvailableCheckTimestamp - val currentTimeStamp = System.currentTimeMillis().toInt() - val interval: Int = corePreferences.checkUpdateAvailableInterval - if (lastTimestamp == 0 || currentTimeStamp - lastTimestamp >= interval) { - val currentVersion = BuildConfig.VERSION_NAME - Log.i("[Dialer] Checking for update using current version [$currentVersion]") - coreContext.core.checkForUpdate(currentVersion) - corePreferences.lastUpdateAvailableCheckTimestamp = currentTimeStamp - } - } - - private fun displayNewVersionAvailableDialog(url: String) { - val viewModel = DialogViewModel(getString(R.string.dialog_update_available)) - val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) - - viewModel.showCancelButton { - dialog.dismiss() - } - - viewModel.showOkButton( - { - try { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e("[Dialer] Can't start ACTION_VIEW intent, IllegalStateException: $ise") - } finally { - dialog.dismiss() - } - }, - getString(R.string.dialog_ok) - ) - - dialog.show() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/ConfigFileViewModel.kt b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/ConfigFileViewModel.kt deleted file mode 100644 index 103511b81..000000000 --- a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/ConfigFileViewModel.kt +++ /dev/null @@ -1,32 +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 . - */ -package org.linphone.activities.main.dialer.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext - -class ConfigFileViewModel : ViewModel() { - val text = MutableLiveData() - - init { - text.value = coreContext.core.config.dump() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt b/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt deleted file mode 100644 index b931023d3..000000000 --- a/app/src/main/java/org/linphone/activities/main/dialer/viewmodels/DialerViewModel.kt +++ /dev/null @@ -1,249 +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 . - */ -package org.linphone.activities.main.dialer.viewmodels - -import android.content.Context -import android.os.Vibrator -import android.text.Editable -import android.widget.EditText -import androidx.lifecycle.MutableLiveData -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.activities.main.dialer.NumpadDigitListener -import org.linphone.activities.main.viewmodels.LogsUploadViewModel -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 DialerViewModel : LogsUploadViewModel() { - val enteredUri = MutableLiveData() - - val atLeastOneCall = MutableLiveData() - - val transferVisibility = MutableLiveData() - - val showPreview = MutableLiveData() - - val showSwitchCamera = MutableLiveData() - - val autoInitiateVideoCalls = MutableLiveData() - - val scheduleConferenceAvailable = MutableLiveData() - - val hideAddContactButton = MutableLiveData() - - val updateAvailableEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val vibrator = coreContext.context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator - - private var addressWaitingNetworkToBeCalled: String? = null - private var timeAtWitchWeTriedToCall: Long = 0 - - private var enteredUriCursorPosition: Int = 0 - - val onKeyClick: NumpadDigitListener = object : NumpadDigitListener { - override fun handleClick(key: Char) { - val sb: StringBuilder = StringBuilder(enteredUri.value) - try { - sb.insert(enteredUriCursorPosition, key.toString()) - } catch (ioobe: IndexOutOfBoundsException) { - sb.insert(sb.length, key.toString()) - } - enteredUri.value = sb.toString() - - if (vibrator.hasVibrator() && corePreferences.dtmfKeypadVibration) { - Compatibility.eventVibration(vibrator) - } - } - - override fun handleLongClick(key: Char): Boolean { - if (key == '1') { - val voiceMailUri = corePreferences.voiceMailUri - if (voiceMailUri != null) { - coreContext.startCall(voiceMailUri) - } - } else { - enteredUri.value += key.toString() - } - return true - } - } - - private val listener = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - atLeastOneCall.value = core.callsNb > 0 - } - - override fun onTransferStateChanged(core: Core, transfered: Call, callState: Call.State) { - if (callState == Call.State.OutgoingProgress) { - // Will work for both blind & attended transfer - onMessageToNotifyEvent.value = Event(org.linphone.R.string.dialer_transfer_succeded) - } - } - - override fun onNetworkReachable(core: Core, reachable: Boolean) { - val address = addressWaitingNetworkToBeCalled.orEmpty() - if (reachable && address.isNotEmpty()) { - val now = System.currentTimeMillis() - if (now - timeAtWitchWeTriedToCall > 1000) { - Log.e( - "[Dialer] More than 1 second has passed waiting for network, abort auto call to $address" - ) - enteredUri.value = address - } else { - Log.i("[Dialer] Network is available, continue auto call to $address") - coreContext.startCall(address) - } - - addressWaitingNetworkToBeCalled = null - timeAtWitchWeTriedToCall = 0 - } - } - - override fun onVersionUpdateCheckResultReceived( - core: Core, - result: VersionUpdateCheckResult, - version: String?, - url: String? - ) { - if (result == VersionUpdateCheckResult.NewVersionAvailable) { - Log.i("[Dialer] Update available, version [$version], url [$url]") - if (!url.isNullOrEmpty()) { - updateAvailableEvent.value = Event(url) - } - } - } - - override fun onAccountRegistrationStateChanged( - core: Core, - account: Account, - state: RegistrationState?, - message: String - ) { - scheduleConferenceAvailable.value = LinphoneUtils.isRemoteConferencingAvailable() - } - } - - init { - coreContext.core.addListener(listener) - - enteredUri.value = "" - atLeastOneCall.value = coreContext.core.callsNb > 0 - transferVisibility.value = false - hideAddContactButton.value = corePreferences.readOnlyNativeContacts - - showSwitchCamera.value = coreContext.showSwitchCameraButton() - scheduleConferenceAvailable.value = LinphoneUtils.isRemoteConferencingAvailable() - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - - super.onCleared() - } - - // This is to workaround the cursor being set to the start when pressing a digit - fun onBeforeUriChanged(editText: EditText, count: Int, after: Int) { - enteredUriCursorPosition = editText.selectionEnd - enteredUriCursorPosition += after - count - } - - fun onAfterUriChanged(editText: EditText, editable: Editable?) { - val newLength = editable?.length ?: 0 - if (newLength <= enteredUriCursorPosition) enteredUriCursorPosition = newLength - if (enteredUriCursorPosition < 0) enteredUriCursorPosition = 0 - editText.setSelection(enteredUriCursorPosition) - } - - fun updateShowVideoPreview() { - val videoPreview = corePreferences.videoPreview - showPreview.value = videoPreview - coreContext.core.isVideoPreviewEnabled = videoPreview - } - - fun eraseLastChar() { - enteredUri.value = enteredUri.value?.dropLast(1) - } - - fun eraseAll(): Boolean { - enteredUri.value = "" - return true - } - - fun directCall(to: String) { - if (coreContext.core.isNetworkReachable) { - coreContext.startCall(to) - } else { - Log.w( - "[Dialer] Network isnt't reachable at the time, wait for network to start call (happens mainly when app is cold started)" - ) - timeAtWitchWeTriedToCall = System.currentTimeMillis() - addressWaitingNetworkToBeCalled = to - } - } - - fun startCall() { - val addressToCall = enteredUri.value.orEmpty() - if (addressToCall.isNotEmpty()) { - coreContext.startCall(addressToCall) - eraseAll() - } else { - setLastOutgoingCallAddress() - } - } - - fun transferCall(): Boolean { - val addressToCall = enteredUri.value.orEmpty() - return if (addressToCall.isNotEmpty()) { - transferCallTo(addressToCall) - eraseAll() - true - } else { - setLastOutgoingCallAddress() - false - } - } - - fun transferCallTo(addressToCall: String) { - if (!coreContext.transferCallTo(addressToCall)) { - onMessageToNotifyEvent.value = Event(org.linphone.R.string.dialer_transfer_failed) - } - } - - fun switchCamera() { - coreContext.switchCamera() - } - - private fun setLastOutgoingCallAddress() { - val callLog = coreContext.core.lastOutgoingCallLog - if (callLog != null) { - enteredUri.value = LinphoneUtils.getDisplayableAddress(callLog.remoteAddress) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/adapters/PdfPagesListAdapter.kt b/app/src/main/java/org/linphone/activities/main/files/adapters/PdfPagesListAdapter.kt deleted file mode 100644 index cfb4b48a2..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/adapters/PdfPagesListAdapter.kt +++ /dev/null @@ -1,52 +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 . - */ -package org.linphone.activities.main.files.adapters - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import org.linphone.R -import org.linphone.activities.main.files.viewmodels.PdfFileViewModel - -class PdfPagesListAdapter(private val pdfViewModel: PdfFileViewModel) : RecyclerView.Adapter() { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder { - val view = LayoutInflater.from(parent.context).inflate( - R.layout.file_pdf_viewer_cell, - parent, - false - ) - return PdfPageViewHolder(view) - } - - override fun getItemCount(): Int { - return pdfViewModel.getPagesCount() - } - - override fun onBindViewHolder(holder: PdfPageViewHolder, position: Int) { - holder.bind(position) - } - - inner class PdfPageViewHolder(private val view: View) : RecyclerView.ViewHolder(view) { - fun bind(index: Int) { - pdfViewModel.loadPdfPageInto(index, view.findViewById(R.id.pdf_image)) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt deleted file mode 100644 index 8dd25f62d..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/AudioViewerFragment.kt +++ /dev/null @@ -1,98 +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 . - */ -package org.linphone.activities.main.files.fragments - -import android.annotation.SuppressLint -import android.os.Bundle -import android.view.KeyEvent -import android.view.View -import android.widget.MediaController -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.findNavController -import org.linphone.R -import org.linphone.activities.main.files.viewmodels.AudioFileViewModel -import org.linphone.activities.main.files.viewmodels.AudioFileViewModelFactory -import org.linphone.core.tools.Log -import org.linphone.databinding.FileAudioViewerFragmentBinding - -class AudioViewerFragment : GenericViewerFragment() { - private lateinit var viewModel: AudioFileViewModel - - private lateinit var mediaController: MediaController - - override fun getLayoutId(): Int = R.layout.file_audio_viewer_fragment - - @SuppressLint("ClickableViewAccessibility") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - val content = sharedViewModel.contentToOpen.value - if (content == null) { - Log.e("[Audio Viewer] Content is null, aborting!") - findNavController().navigateUp() - return - } - - viewModel = ViewModelProvider( - this, - AudioFileViewModelFactory(content) - )[AudioFileViewModel::class.java] - binding.viewModel = viewModel - - mediaController = object : MediaController(requireContext()) { - // This hack is even if media controller is showed with timeout=0 - // Once a control is touched, it will disappear 3 seconds later anyway - override fun show(timeout: Int) { - super.show(0) - } - - // This is to prevent the first back key press to only hide to media controls - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - if (event?.keyCode == KeyEvent.KEYCODE_BACK) { - goBack() - return true - } - return super.dispatchKeyEvent(event) - } - } - mediaController.setMediaPlayer(viewModel) - - viewModel.mediaPlayer.setOnPreparedListener { - mediaController.setAnchorView(binding.anchor) - // This will make the controls visible forever - mediaController.show(0) - } - } - - override fun onPause() { - mediaController.hide() - viewModel.mediaPlayer.pause() - - super.onPause() - } - - override fun onResume() { - super.onResume() - - mediaController.show(0) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/GenericViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/GenericViewerFragment.kt deleted file mode 100644 index 63ef1b4f5..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/GenericViewerFragment.kt +++ /dev/null @@ -1,51 +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 . - */ -package org.linphone.activities.main.files.fragments - -import android.os.Bundle -import android.view.View -import androidx.databinding.ViewDataBinding -import androidx.navigation.fragment.findNavController -import org.linphone.R -import org.linphone.activities.main.fragments.SecureFragment -import org.linphone.core.tools.Log - -abstract class GenericViewerFragment : SecureFragment() { - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - isSecure = arguments?.getBoolean("Secure") ?: false - } - - override fun onStart() { - super.onStart() - - val content = sharedViewModel.contentToOpen.value - if (content == null) { - Log.e("[Generic Viewer] Content is null, aborting!") - findNavController().navigateUp() - return - } - - (childFragmentManager.findFragmentById(R.id.top_bar_fragment) as? TopBarFragment) - ?.setContent(content) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt deleted file mode 100644 index 4cf1dad5e..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/ImageViewerFragment.kt +++ /dev/null @@ -1,72 +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 . - */ -package org.linphone.activities.main.files.fragments - -import android.annotation.SuppressLint -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.MainActivity -import org.linphone.activities.main.files.viewmodels.ImageFileViewModel -import org.linphone.activities.main.files.viewmodels.ImageFileViewModelFactory -import org.linphone.compatibility.Compatibility -import org.linphone.core.tools.Log -import org.linphone.databinding.FileImageViewerFragmentBinding - -class ImageViewerFragment : GenericViewerFragment() { - private lateinit var viewModel: ImageFileViewModel - - override fun getLayoutId(): Int = R.layout.file_image_viewer_fragment - - @SuppressLint("ClickableViewAccessibility") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - val content = sharedViewModel.contentToOpen.value - if (content == null) { - Log.e("[Image Viewer] Content is null, aborting!") - findNavController().navigateUp() - return - } - - viewModel = ViewModelProvider( - this, - ImageFileViewModelFactory(content) - )[ImageFileViewModel::class.java] - binding.viewModel = viewModel - - viewModel.fullScreenMode.observe( - viewLifecycleOwner - ) { hide -> - Compatibility.hideAndroidSystemUI(hide, requireActivity().window) - (requireActivity() as MainActivity).hideStatusFragment(hide) - } - } - - override fun onPause() { - super.onPause() - - viewModel.fullScreenMode.value = false - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/PdfViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/PdfViewerFragment.kt deleted file mode 100644 index 2e123d104..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/PdfViewerFragment.kt +++ /dev/null @@ -1,64 +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 . - */ -package org.linphone.activities.main.files.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.files.adapters.PdfPagesListAdapter -import org.linphone.activities.main.files.viewmodels.PdfFileViewModel -import org.linphone.activities.main.files.viewmodels.PdfFileViewModelFactory -import org.linphone.core.tools.Log -import org.linphone.databinding.FilePdfViewerFragmentBinding - -class PdfViewerFragment : GenericViewerFragment() { - private lateinit var viewModel: PdfFileViewModel - private lateinit var adapter: PdfPagesListAdapter - - override fun getLayoutId(): Int = R.layout.file_pdf_viewer_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - val content = sharedViewModel.contentToOpen.value - if (content == null) { - Log.e("[PDF Viewer] Content is null, aborting!") - findNavController().navigateUp() - return - } - - viewModel = ViewModelProvider( - this, - PdfFileViewModelFactory(content) - )[PdfFileViewModel::class.java] - binding.viewModel = viewModel - - viewModel.rendererReady.observe(viewLifecycleOwner) { - it.consume { - adapter = PdfPagesListAdapter(viewModel) - binding.pdfViewPager.adapter = adapter - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt deleted file mode 100644 index 470ce50c1..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/TextViewerFragment.kt +++ /dev/null @@ -1,55 +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 . - */ -package org.linphone.activities.main.files.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.files.viewmodels.TextFileViewModel -import org.linphone.activities.main.files.viewmodels.TextFileViewModelFactory -import org.linphone.core.tools.Log -import org.linphone.databinding.FileTextViewerFragmentBinding - -class TextViewerFragment : GenericViewerFragment() { - private lateinit var viewModel: TextFileViewModel - - override fun getLayoutId(): Int = R.layout.file_text_viewer_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - val content = sharedViewModel.contentToOpen.value - if (content == null) { - Log.e("[Text Viewer] Content is null, aborting!") - findNavController().navigateUp() - return - } - - viewModel = ViewModelProvider( - this, - TextFileViewModelFactory(content) - )[TextFileViewModel::class.java] - binding.viewModel = viewModel - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt deleted file mode 100644 index e956f4654..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/TopBarFragment.kt +++ /dev/null @@ -1,184 +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 . - */ -package org.linphone.activities.main.files.fragments - -import android.os.Bundle -import android.view.View -import android.webkit.MimeTypeMap -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.SnackBarActivity -import org.linphone.compatibility.Compatibility -import org.linphone.core.Content -import org.linphone.core.tools.Log -import org.linphone.databinding.FileViewerTopBarFragmentBinding -import org.linphone.utils.FileUtils -import org.linphone.utils.PermissionHelper - -class TopBarFragment : GenericFragment() { - private var content: Content? = null - private var plainFilePath: String = "" - - override fun getLayoutId(): Int = R.layout.file_viewer_top_bar_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - useMaterialSharedAxisXForwardAnimation = false - - binding.setExportClickListener { - val contentToExport = content - if (contentToExport != null) { - exportContent(contentToExport) - } else { - Log.e("[File Viewer] No Content set!") - } - } - } - - override fun onSaveInstanceState(outState: Bundle) { - outState.putString("FilePath", plainFilePath) - super.onSaveInstanceState(outState) - } - - override fun onViewStateRestored(savedInstanceState: Bundle?) { - super.onViewStateRestored(savedInstanceState) - plainFilePath = savedInstanceState?.getString("FilePath") ?: plainFilePath - } - - override fun onDestroyView() { - if (plainFilePath.isNotEmpty() && plainFilePath != content?.filePath.orEmpty()) { - Log.i("[File Viewer] Destroying plain file path: $plainFilePath") - FileUtils.deleteFile(plainFilePath) - } - super.onDestroyView() - } - - fun setContent(c: Content) { - Log.i("[File Viewer] Content file path is: ${c.filePath}") - content = c - binding.fileName.text = c.name - } - - private fun exportContent(content: Content) { - lifecycleScope.launch { - var mediaStoreFilePath = "" - if (PermissionHelper.get().hasWriteExternalStoragePermission()) { - val filePath = content.filePath.orEmpty() - Log.i("[File Viewer] Trying to export file [$filePath] through Media Store API") - - val extension = FileUtils.getExtensionFromFileName(filePath) - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - when (FileUtils.getMimeType(mime)) { - FileUtils.MimeType.Image -> { - val export = lifecycleScope.async { - Compatibility.addImageToMediaStore(requireContext(), content) - } - if (export.await()) { - Log.i( - "[File Viewer] Successfully exported image [${content.name}] to Media Store: ${content.userData}" - ) - mediaStoreFilePath = content.userData.toString() - } else { - Log.e( - "[File Viewer] Something went wrong while copying file to Media Store..." - ) - } - } - FileUtils.MimeType.Video -> { - val export = lifecycleScope.async { - Compatibility.addVideoToMediaStore(requireContext(), content) - } - if (export.await()) { - Log.i( - "[File Viewer] Successfully exported video [${content.name}] to Media Store: ${content.userData}" - ) - mediaStoreFilePath = content.userData.toString() - } else { - Log.e( - "[File Viewer] Something went wrong while copying file to Media Store..." - ) - } - } - FileUtils.MimeType.Audio -> { - val export = lifecycleScope.async { - Compatibility.addAudioToMediaStore(requireContext(), content) - } - if (export.await()) { - Log.i( - "[File Viewer] Successfully exported audio [${content.name}] to Media Store: ${content.userData}" - ) - mediaStoreFilePath = content.userData.toString() - } else { - Log.e( - "[File Viewer] Something went wrong while copying file to Media Store..." - ) - } - } - else -> { - Log.w( - "[File Viewer] File [${content.name}] isn't either an image, an audio file or a video, can't add it to the Media Store" - ) - } - } - } else { - Log.w( - "[File Viewer] Can't export image through Media Store API (requires Android 10 or WRITE_EXTERNAL permission, using fallback method..." - ) - } - - withContext(Dispatchers.Main) { - if (mediaStoreFilePath.isEmpty()) { - Log.w( - "[File Viewer] Media store file path is empty, media store export failed?" - ) - - val filePath = content.exportPlainFile().orEmpty() - plainFilePath = filePath.ifEmpty { content.filePath.orEmpty() } - Log.i("[File Viewer] Plain file path is: $plainFilePath") - if (plainFilePath.isNotEmpty()) { - if (!FileUtils.openFileInThirdPartyApp(requireActivity(), plainFilePath)) { - (requireActivity() as SnackBarActivity).showSnackBar( - R.string.chat_message_no_app_found_to_handle_file_mime_type - ) - if (plainFilePath != content.filePath.orEmpty()) { - Log.i( - "[File Viewer] No app to open plain file path [$plainFilePath], destroying it" - ) - FileUtils.deleteFile(plainFilePath) - } - plainFilePath = "" - } - } - } else { - plainFilePath = "" - Log.i("[File Viewer] Media store file path is: $mediaStoreFilePath") - FileUtils.openMediaStoreFile(requireActivity(), mediaStoreFilePath) - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt b/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt deleted file mode 100644 index 1840b0f12..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/fragments/VideoViewerFragment.kt +++ /dev/null @@ -1,128 +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 . - */ -package org.linphone.activities.main.files.fragments - -import android.os.Bundle -import android.view.KeyEvent -import android.view.View -import android.widget.MediaController -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.findNavController -import org.linphone.R -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.files.viewmodels.VideoFileViewModel -import org.linphone.activities.main.files.viewmodels.VideoFileViewModelFactory -import org.linphone.compatibility.Compatibility -import org.linphone.core.tools.Log -import org.linphone.databinding.FileVideoViewerFragmentBinding - -class VideoViewerFragment : GenericViewerFragment() { - private lateinit var viewModel: VideoFileViewModel - - private lateinit var mediaController: MediaController - - override fun getLayoutId(): Int = R.layout.file_video_viewer_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - val content = sharedViewModel.contentToOpen.value - if (content == null) { - Log.e("[Video Viewer] Content is null, aborting!") - findNavController().navigateUp() - return - } - - viewModel = ViewModelProvider( - this, - VideoFileViewModelFactory(content) - )[VideoFileViewModel::class.java] - binding.viewModel = viewModel - - mediaController = object : MediaController(requireContext()) { - // This is to prevent the first back key press to only hide to media controls - override fun dispatchKeyEvent(event: KeyEvent?): Boolean { - if (event?.keyCode == KeyEvent.KEYCODE_BACK) { - goBack() - return true - } - return super.dispatchKeyEvent(event) - } - } - initMediaController() - - viewModel.fullScreenMode.observe( - viewLifecycleOwner - ) { hide -> - Compatibility.hideAndroidSystemUI(hide, requireActivity().window) - (requireActivity() as MainActivity).hideStatusFragment(hide) - } - } - - override fun onResume() { - super.onResume() - - binding.videoView.start() - } - - override fun onPause() { - if (mediaController.isShowing) { - mediaController.hide() - } - - if (binding.videoView.isPlaying) { - binding.videoView.pause() - } - - viewModel.fullScreenMode.value = false - super.onPause() - } - - override fun onDestroyView() { - binding.videoView.stopPlayback() - - super.onDestroyView() - } - - private fun initMediaController() { - val videoView = binding.videoView - - videoView.setOnPreparedListener { mediaPlayer -> - mediaPlayer.setOnVideoSizeChangedListener { _, _, _ -> - videoView.setMediaController(mediaController) - // The following will make the video controls above the video - // mediaController.setAnchorView(videoView) - - // This will make the controls visible right away for 3 seconds - // If 0 as timeout, they will stay visible mediaController.hide() is called - mediaController.show() - } - } - - videoView.setOnErrorListener { _, what, extra -> - Log.e("[Video Viewer] Error: $what ($extra)") - false - } - - videoView.setVideoPath(viewModel.filePath) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/AudioFileViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/AudioFileViewModel.kt deleted file mode 100644 index 33e83b7d9..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/AudioFileViewModel.kt +++ /dev/null @@ -1,106 +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 . - */ -package org.linphone.activities.main.files.viewmodels - -import android.media.AudioAttributes -import android.media.MediaPlayer -import android.widget.MediaController -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import java.lang.IllegalStateException -import org.linphone.core.Content - -class AudioFileViewModelFactory(private val content: Content) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return AudioFileViewModel(content) as T - } -} - -class AudioFileViewModel(content: Content) : FileViewerViewModel(content), MediaController.MediaPlayerControl { - val mediaPlayer = MediaPlayer() - - init { - mediaPlayer.apply { - setAudioAttributes( - AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_MUSIC).setUsage( - AudioAttributes.USAGE_MEDIA - ).build() - ) - setDataSource(filePath) - prepare() - start() - } - } - - override fun onCleared() { - mediaPlayer.release() - super.onCleared() - } - - override fun start() { - mediaPlayer.start() - } - - override fun pause() { - mediaPlayer.pause() - } - - override fun getDuration(): Int { - return mediaPlayer.duration - } - - override fun getCurrentPosition(): Int { - try { - return mediaPlayer.currentPosition - } catch (_: IllegalStateException) {} - return 0 - } - - override fun seekTo(pos: Int) { - mediaPlayer.seekTo(pos) - } - - override fun isPlaying(): Boolean { - return mediaPlayer.isPlaying - } - - override fun getBufferPercentage(): Int { - return 0 - } - - override fun canPause(): Boolean { - return true - } - - override fun canSeekBackward(): Boolean { - return true - } - - override fun canSeekForward(): Boolean { - return true - } - - override fun getAudioSessionId(): Int { - return mediaPlayer.audioSessionId - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/FileViewerViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/FileViewerViewModel.kt deleted file mode 100644 index d9f46d86c..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/FileViewerViewModel.kt +++ /dev/null @@ -1,57 +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 . - */ -package org.linphone.activities.main.files.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.core.Content -import org.linphone.core.tools.Log -import org.linphone.utils.FileUtils - -open class FileViewerViewModel(val content: Content) : ViewModel() { - val filePath: String - private val deleteAfterUse: Boolean = content.isFileEncrypted - - val fullScreenMode = MutableLiveData() - - init { - filePath = if (deleteAfterUse) { - Log.i( - "[File Viewer] [VFS] Content is encrypted, requesting plain file path for file [${content.filePath}]" - ) - content.exportPlainFile() - } else { - content.filePath.orEmpty() - } - } - - override fun onCleared() { - if (deleteAfterUse) { - Log.i("[File Viewer] [VFS] Deleting temporary plain file [$filePath]") - FileUtils.deleteFile(filePath) - } - - super.onCleared() - } - - fun toggleFullScreen() { - fullScreenMode.value = fullScreenMode.value != true - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/ImageFileViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/ImageFileViewModel.kt deleted file mode 100644 index bddf64d5e..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/ImageFileViewModel.kt +++ /dev/null @@ -1,35 +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 . - */ -package org.linphone.activities.main.files.viewmodels - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import org.linphone.core.Content - -class ImageFileViewModelFactory(private val content: Content) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return ImageFileViewModel(content) as T - } -} - -class ImageFileViewModel(content: Content) : FileViewerViewModel(content) diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/PdfFileViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/PdfFileViewModel.kt deleted file mode 100644 index 259957fac..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/PdfFileViewModel.kt +++ /dev/null @@ -1,114 +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 . - */ -package org.linphone.activities.main.files.viewmodels - -import android.graphics.Bitmap -import android.graphics.pdf.PdfRenderer -import android.os.ParcelFileDescriptor -import android.widget.ImageView -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import java.io.File -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.Content -import org.linphone.core.tools.Log -import org.linphone.utils.Event - -class PdfFileViewModelFactory(private val content: Content) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return PdfFileViewModel(content) as T - } -} - -class PdfFileViewModel(content: Content) : FileViewerViewModel(content) { - val operationInProgress = MutableLiveData() - - val rendererReady = MutableLiveData>() - - private lateinit var pdfRenderer: PdfRenderer - - init { - operationInProgress.value = false - - viewModelScope.launch { - withContext(Dispatchers.IO) { - val input = ParcelFileDescriptor.open( - File(filePath), - ParcelFileDescriptor.MODE_READ_ONLY - ) - pdfRenderer = PdfRenderer(input) - Log.i("[PDF Viewer] ${pdfRenderer.pageCount} pages in file $filePath") - rendererReady.postValue(Event(true)) - } - } - } - - override fun onCleared() { - if (this::pdfRenderer.isInitialized) { - pdfRenderer.close() - } - super.onCleared() - } - - fun getPagesCount(): Int { - if (this::pdfRenderer.isInitialized) { - return pdfRenderer.pageCount - } - return 0 - } - - fun loadPdfPageInto(index: Int, view: ImageView) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - try { - operationInProgress.postValue(true) - - val page: PdfRenderer.Page = pdfRenderer.openPage(index) - val width = - if (coreContext.screenWidth <= coreContext.screenHeight) coreContext.screenWidth else coreContext.screenHeight - val bm = Bitmap.createBitmap( - width.toInt(), - (width / page.width * page.height).toInt(), - Bitmap.Config.ARGB_8888 - ) - page.render(bm, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY) - page.close() - - withContext(Dispatchers.Main) { - view.setImageBitmap(bm) - } - - operationInProgress.postValue(false) - } catch (e: Exception) { - Log.e("[PDF Viewer] Exception: $e") - operationInProgress.postValue(false) - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/TextFileViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/TextFileViewModel.kt deleted file mode 100644 index 56aa44dbc..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/TextFileViewModel.kt +++ /dev/null @@ -1,72 +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 . - */ -package org.linphone.activities.main.files.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import java.io.BufferedReader -import java.io.FileReader -import java.lang.StringBuilder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.linphone.core.Content -import org.linphone.core.tools.Log - -class TextFileViewModelFactory(private val content: Content) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return TextFileViewModel(content) as T - } -} - -class TextFileViewModel(content: Content) : FileViewerViewModel(content) { - val operationInProgress = MutableLiveData() - - val text = MutableLiveData() - - init { - viewModelScope.launch { - withContext(Dispatchers.IO) { - try { - operationInProgress.postValue(true) - val br = BufferedReader(FileReader(filePath)) - var line: String? - val textBuilder = StringBuilder() - while (br.readLine().also { line = it } != null) { - textBuilder.append(line) - textBuilder.append('\n') - } - br.close() - - text.postValue(textBuilder.toString()) - operationInProgress.postValue(false) - } catch (e: Exception) { - Log.e("[Text Viewer] Exception: $e") - operationInProgress.postValue(false) - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/files/viewmodels/VideoFileViewModel.kt b/app/src/main/java/org/linphone/activities/main/files/viewmodels/VideoFileViewModel.kt deleted file mode 100644 index b9510c0cd..000000000 --- a/app/src/main/java/org/linphone/activities/main/files/viewmodels/VideoFileViewModel.kt +++ /dev/null @@ -1,35 +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 . - */ -package org.linphone.activities.main.files.viewmodels - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import org.linphone.core.Content - -class VideoFileViewModelFactory(private val content: Content) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return VideoFileViewModel(content) as T - } -} - -class VideoFileViewModel(content: Content) : FileViewerViewModel(content) diff --git a/app/src/main/java/org/linphone/activities/main/fragments/EmptyFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/EmptyFragment.kt deleted file mode 100644 index b9fcfe1cb..000000000 --- a/app/src/main/java/org/linphone/activities/main/fragments/EmptyFragment.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import com.google.android.material.transition.MaterialSharedAxis -import org.linphone.R - -class EmptyFragment : Fragment() { - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false) - return inflater.inflate(R.layout.empty_fragment, container, false) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/fragments/ListTopBarFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/ListTopBarFragment.kt deleted file mode 100644 index d18ed62a7..000000000 --- a/app/src/main/java/org/linphone/activities/main/fragments/ListTopBarFragment.kt +++ /dev/null @@ -1,61 +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 . - */ -package org.linphone.activities.main.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.main.viewmodels.ListTopBarViewModel -import org.linphone.databinding.ListEditTopBarFragmentBinding -import org.linphone.utils.Event - -class ListTopBarFragment : GenericFragment() { - private lateinit var viewModel: ListTopBarViewModel - - override fun getLayoutId(): Int = R.layout.list_edit_top_bar_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - useMaterialSharedAxisXForwardAnimation = false - - viewModel = ViewModelProvider(parentFragment ?: this)[ListTopBarViewModel::class.java] - binding.viewModel = viewModel - - binding.setCancelClickListener { - viewModel.isEditionEnabled.value = false - } - - binding.setSelectAllClickListener { - viewModel.selectAllEvent.value = Event(true) - } - - binding.setUnSelectAllClickListener { - viewModel.unSelectAllEvent.value = Event(true) - } - - binding.setDeleteClickListener { - viewModel.deleteSelectionEvent.value = Event(true) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt deleted file mode 100644 index b38de9c08..000000000 --- a/app/src/main/java/org/linphone/activities/main/fragments/MasterFragment.kt +++ /dev/null @@ -1,169 +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 . - */ -package org.linphone.activities.main.fragments - -import android.app.Dialog -import android.os.Bundle -import android.view.View -import androidx.activity.OnBackPressedCallback -import androidx.core.view.doOnPreDraw -import androidx.databinding.ViewDataBinding -import androidx.lifecycle.ViewModelProvider -import androidx.slidingpanelayout.widget.SlidingPaneLayout -import org.linphone.R -import org.linphone.activities.main.adapters.SelectionListAdapter -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.main.viewmodels.ListTopBarViewModel -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils -import org.linphone.utils.DialogUtils -import org.linphone.utils.hideKeyboard - -/** - * This fragment can be inherited by all fragments that will display a list - * where items can be selected for removal through the ListTopBarFragment - */ -abstract class MasterFragment> : SecureFragment() { - protected var _adapter: U? = null - protected val adapter: U - get() { - if (_adapter == null) { - Log.e("[Master Fragment] Attempting to get a null adapter!") - } - return _adapter!! - } - - protected lateinit var listSelectionViewModel: ListTopBarViewModel - protected open val dialogConfirmationMessageBeforeRemoval: Int = R.plurals.dialog_default_delete - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - // List selection - listSelectionViewModel = ViewModelProvider(this)[ListTopBarViewModel::class.java] - - listSelectionViewModel.isEditionEnabled.observe( - viewLifecycleOwner - ) { - if (!it) listSelectionViewModel.onUnSelectAll() - } - - listSelectionViewModel.selectAllEvent.observe( - viewLifecycleOwner - ) { - it.consume { - listSelectionViewModel.onSelectAll(getItemCount() - 1) - } - } - - listSelectionViewModel.unSelectAllEvent.observe( - viewLifecycleOwner - ) { - it.consume { - listSelectionViewModel.onUnSelectAll() - } - } - - listSelectionViewModel.deleteSelectionEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val confirmationDialog = AppUtils.getStringWithPlural( - dialogConfirmationMessageBeforeRemoval, - listSelectionViewModel.selectedItems.value.orEmpty().size - ) - val viewModel = DialogViewModel(confirmationDialog) - val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel) - - viewModel.showCancelButton { - dialog.dismiss() - listSelectionViewModel.isEditionEnabled.value = false - } - - viewModel.showDeleteButton( - { - delete() - dialog.dismiss() - listSelectionViewModel.isEditionEnabled.value = false - }, - getString(R.string.dialog_delete) - ) - - dialog.show() - } - } - } - - fun setUpSlidingPane(slidingPane: SlidingPaneLayout) { - binding.root.doOnPreDraw { - sharedViewModel.isSlidingPaneSlideable.value = slidingPane.isSlideable - - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - SlidingPaneBackPressedCallback(slidingPane) - ) - } - - slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED - } - - private fun delete() { - val list = listSelectionViewModel.selectedItems.value ?: arrayListOf() - deleteItems(list) - } - - private fun getItemCount(): Int { - return adapter.itemCount - } - - abstract fun deleteItems(indexesOfItemToDelete: ArrayList) - - class SlidingPaneBackPressedCallback(private val slidingPaneLayout: SlidingPaneLayout) : - OnBackPressedCallback( - slidingPaneLayout.isSlideable && slidingPaneLayout.isOpen - ), - SlidingPaneLayout.PanelSlideListener { - - init { - Log.d( - "[Master Fragment] SlidingPane isSlideable = ${slidingPaneLayout.isSlideable}, isOpen = ${slidingPaneLayout.isOpen}" - ) - slidingPaneLayout.addPanelSlideListener(this) - } - - override fun handleOnBackPressed() { - Log.d("[Master Fragment] handleOnBackPressed, closing sliding pane") - slidingPaneLayout.hideKeyboard() - slidingPaneLayout.closePane() - } - - override fun onPanelOpened(panel: View) { - Log.d("[Master Fragment] onPanelOpened") - isEnabled = true - } - - override fun onPanelClosed(panel: View) { - Log.d("[Master Fragment] onPanelClosed") - isEnabled = false - } - - override fun onPanelSlide(panel: View, slideOffset: Float) { } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/fragments/SecureFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/SecureFragment.kt deleted file mode 100644 index 7cc660755..000000000 --- a/app/src/main/java/org/linphone/activities/main/fragments/SecureFragment.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.fragments - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import androidx.core.view.ViewCompat -import androidx.databinding.ViewDataBinding -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.activities.GenericFragment -import org.linphone.core.tools.Log - -abstract class SecureFragment : GenericFragment() { - protected var isSecure: Boolean = false - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Assume we might want to be secure to prevent quick visible blink while screen recording. - enableSecureMode(true) - return super.onCreateView(inflater, container, savedInstanceState) - } - - override fun onResume() { - if (isSecure) { - enableSecureMode(true) - } else { - // This is a workaround to prevent a small blink showing the previous secured screen - lifecycleScope.launch { - withContext(Dispatchers.Main) { - delay(200) - enableSecureMode(isSecure) - } - } - } - super.onResume() - } - - private fun enableSecureMode(enable: Boolean) { - if (corePreferences.disableSecureMode) { - Log.d("[Secure Fragment] Disabling secure flag on window due to setting") - return - } - - Log.d("[Secure Fragment] ${if (enable) "Enabling" else "Disabling"} secure flag on window") - val window = requireActivity().window - val windowManager = requireActivity().windowManager - - val flags: Int = window.attributes.flags - if ((enable && flags and WindowManager.LayoutParams.FLAG_SECURE != 0) || - (!enable && flags and WindowManager.LayoutParams.FLAG_SECURE == 0) - ) { - Log.d( - "[Secure Fragment] Secure flag is already ${if (enable) "enabled" else "disabled"}, skipping..." - ) - return - } - - if (enable) { - window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) - } - - if (ViewCompat.isAttachedToWindow(window.decorView)) { - Log.d("[Secure Fragment] Redrawing window decorView to apply flag") - try { - windowManager.updateViewLayout(window.decorView, window.attributes) - } catch (ise: IllegalStateException) { - Log.e("[Secure Fragment] Failed to update view layout: $ise") - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/fragments/StatusFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/StatusFragment.kt deleted file mode 100644 index 50b357198..000000000 --- a/app/src/main/java/org/linphone/activities/main/fragments/StatusFragment.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.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.main.viewmodels.StatusViewModel -import org.linphone.core.tools.Log -import org.linphone.databinding.StatusFragmentBinding -import org.linphone.utils.Event - -class StatusFragment : GenericFragment() { - private lateinit var viewModel: StatusViewModel - - override fun getLayoutId(): Int = R.layout.status_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - useMaterialSharedAxisXForwardAnimation = false - - viewModel = ViewModelProvider(this)[StatusViewModel::class.java] - binding.viewModel = viewModel - - sharedViewModel.accountRemoved.observe( - viewLifecycleOwner - ) { - Log.i("[Status Fragment] An account was removed, update default account state") - val defaultAccount = coreContext.core.defaultAccount - if (defaultAccount != null) { - viewModel.updateDefaultAccountRegistrationStatus(defaultAccount.state) - } - } - - binding.setMenuClickListener { - sharedViewModel.toggleDrawerEvent.value = Event(true) - } - - binding.setRefreshClickListener { - viewModel.refreshRegister() - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/fragments/TabsFragment.kt b/app/src/main/java/org/linphone/activities/main/fragments/TabsFragment.kt deleted file mode 100644 index b62708eb3..000000000 --- a/app/src/main/java/org/linphone/activities/main/fragments/TabsFragment.kt +++ /dev/null @@ -1,148 +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 . - */ -package org.linphone.activities.main.fragments - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.NavController -import androidx.navigation.NavDestination -import androidx.navigation.fragment.findNavController -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.main.viewmodels.TabsViewModel -import org.linphone.activities.navigateToCallHistory -import org.linphone.activities.navigateToChatRooms -import org.linphone.activities.navigateToContacts -import org.linphone.activities.navigateToDialer -import org.linphone.databinding.TabsFragmentBinding -import org.linphone.utils.Event - -class TabsFragment : GenericFragment(), NavController.OnDestinationChangedListener { - private lateinit var viewModel: TabsViewModel - - override fun getLayoutId(): Int = R.layout.tabs_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - useMaterialSharedAxisXForwardAnimation = false - - viewModel = requireActivity().run { - ViewModelProvider(this)[TabsViewModel::class.java] - } - binding.viewModel = viewModel - - binding.setHistoryClickListener { - when (findNavController().currentDestination?.id) { - R.id.masterContactsFragment -> sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event( - R.id.masterCallLogsFragment - ) - R.id.dialerFragment -> sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event( - R.id.masterCallLogsFragment - ) - } - navigateToCallHistory() - } - - binding.setContactsClickListener { - when (findNavController().currentDestination?.id) { - R.id.dialerFragment -> sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event( - R.id.masterContactsFragment - ) - } - sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event( - findNavController().currentDestination?.id ?: -1 - ) - navigateToContacts() - } - - binding.setDialerClickListener { - when (findNavController().currentDestination?.id) { - R.id.masterContactsFragment -> sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event( - R.id.dialerFragment - ) - } - sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event( - findNavController().currentDestination?.id ?: -1 - ) - navigateToDialer() - } - - binding.setChatClickListener { - when (findNavController().currentDestination?.id) { - R.id.masterContactsFragment -> sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event( - R.id.masterChatRoomsFragment - ) - R.id.dialerFragment -> sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event( - R.id.masterChatRoomsFragment - ) - } - navigateToChatRooms() - } - } - - override fun onStart() { - super.onStart() - findNavController().addOnDestinationChangedListener(this) - } - - override fun onStop() { - findNavController().removeOnDestinationChangedListener(this) - super.onStop() - } - - override fun onDestinationChanged( - controller: NavController, - destination: NavDestination, - arguments: Bundle? - ) { - if (corePreferences.enableAnimations) { - when (destination.id) { - R.id.masterCallLogsFragment -> binding.motionLayout.transitionToState( - R.id.call_history - ) - R.id.masterContactsFragment -> binding.motionLayout.transitionToState(R.id.contacts) - R.id.dialerFragment -> binding.motionLayout.transitionToState(R.id.dialer) - R.id.masterChatRoomsFragment -> binding.motionLayout.transitionToState( - R.id.chat_rooms - ) - } - } else { - when (destination.id) { - R.id.masterCallLogsFragment -> binding.motionLayout.setTransition( - R.id.call_history, - R.id.call_history - ) - R.id.masterContactsFragment -> binding.motionLayout.setTransition( - R.id.contacts, - R.id.contacts - ) - R.id.dialerFragment -> binding.motionLayout.setTransition(R.id.dialer, R.id.dialer) - R.id.masterChatRoomsFragment -> binding.motionLayout.setTransition( - R.id.chat_rooms, - R.id.chat_rooms - ) - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt deleted file mode 100644 index 4c20b767e..000000000 --- a/app/src/main/java/org/linphone/activities/main/history/adapters/CallLogsListAdapter.kt +++ /dev/null @@ -1,167 +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 . - */ -package org.linphone.activities.main.history.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.history.data.GroupedCallLogData -import org.linphone.activities.main.viewmodels.ListTopBarViewModel -import org.linphone.databinding.GenericListHeaderBinding -import org.linphone.databinding.HistoryListCellBinding -import org.linphone.utils.* - -class CallLogsListAdapter( - selectionVM: ListTopBarViewModel, - private val viewLifecycleOwner: LifecycleOwner -) : SelectionListAdapter( - selectionVM, - CallLogDiffCallback() -), - HeaderAdapter { - val selectedCallLogEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val startCallToEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val binding: HistoryListCellBinding = DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.history_list_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: HistoryListCellBinding - ) : RecyclerView.ViewHolder(binding.root) { - fun bind(callLogGroup: GroupedCallLogData) { - with(binding) { - val callLogViewModel = callLogGroup.lastCallLogViewModel - viewModel = callLogViewModel - - 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 { - startCallToEvent.value = Event(callLogGroup) - } - } - - setLongClickListener { - if (selectionViewModel.isEditionEnabled.value == false) { - selectionViewModel.isEditionEnabled.value = true - // Selection will be handled by click listener - true - } - false - } - - // This listener is disabled when in edition mode - setDetailsClickListener { - selectedCallLogEvent.value = Event(callLogGroup) - } - - groupCount = callLogGroup.callLogs.size - - executePendingBindings() - } - } - } - - override fun displayHeaderForPosition(position: Int): Boolean { - if (position >= itemCount) return false - val callLogGroup = getItem(position) - val date = callLogGroup.lastCallLogStartTimestamp - val previousPosition = position - 1 - return if (previousPosition >= 0) { - val previousItemDate = getItem(previousPosition).lastCallLogStartTimestamp - !TimestampUtils.isSameDay(date, previousItemDate) - } else { - true - } - } - - override fun getHeaderViewForPosition(context: Context, position: Int): View { - val callLog = getItem(position) - val date = formatDate(context, callLog.lastCallLogStartTimestamp) - val binding: GenericListHeaderBinding = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.generic_list_header, - null, - false - ) - binding.title = date - binding.executePendingBindings() - return binding.root - } - - private fun formatDate(context: Context, date: Long): String { - if (TimestampUtils.isToday(date)) { - return context.getString(R.string.today) - } else if (TimestampUtils.isYesterday(date)) { - return context.getString(R.string.yesterday) - } - return TimestampUtils.toString(date, onlyDate = true, shortDate = false, hideYear = false) - } -} - -private class CallLogDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: GroupedCallLogData, - newItem: GroupedCallLogData - ): Boolean { - return oldItem.lastCallLogId == newItem.lastCallLogId - } - - override fun areContentsTheSame( - oldItem: GroupedCallLogData, - newItem: GroupedCallLogData - ): Boolean { - return oldItem.callLogs.size == newItem.callLogs.size - } -} diff --git a/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt b/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt deleted file mode 100644 index 3fa709f43..000000000 --- a/app/src/main/java/org/linphone/activities/main/history/data/GroupedCallLogData.kt +++ /dev/null @@ -1,53 +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 . - */ -package org.linphone.activities.main.history.data - -import org.linphone.activities.main.history.viewmodels.CallLogViewModel -import org.linphone.core.CallLog - -class GroupedCallLogData(callLog: CallLog) { - val callLogs = arrayListOf(callLog) - - var lastCallLog: CallLog = callLog - var lastCallLogId: String? = callLog.callId - var lastCallLogStartTimestamp: Long = callLog.startDate - val lastCallLogViewModel: CallLogViewModel - get() { - if (::_lastCallLogViewModel.isInitialized) { - return _lastCallLogViewModel - } - _lastCallLogViewModel = CallLogViewModel(lastCallLog) - return _lastCallLogViewModel - } - - private lateinit var _lastCallLogViewModel: CallLogViewModel - - fun destroy() { - if (::_lastCallLogViewModel.isInitialized) { - lastCallLogViewModel - } - } - - fun updateLastCallLog(callLog: CallLog) { - lastCallLog = callLog - lastCallLogId = callLog.callId - lastCallLogStartTimestamp = callLog.startDate - } -} diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt deleted file mode 100644 index 8bb760ec9..000000000 --- a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailCallLogFragment.kt +++ /dev/null @@ -1,152 +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 . - */ -package org.linphone.activities.main.history.fragments - -import android.os.Bundle -import android.view.View -import androidx.navigation.fragment.findNavController -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.* -import org.linphone.activities.main.* -import org.linphone.activities.main.history.viewmodels.CallLogViewModel -import org.linphone.activities.navigateToContacts -import org.linphone.core.tools.Log -import org.linphone.databinding.HistoryDetailFragmentBinding -import org.linphone.utils.Event - -class DetailCallLogFragment : GenericFragment() { - private lateinit var viewModel: CallLogViewModel - - override fun getLayoutId(): Int = R.layout.history_detail_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.sharedMainViewModel = sharedViewModel - - val callLogGroup = sharedViewModel.selectedCallLogGroup.value - if (callLogGroup == null) { - Log.e("[History] Call log group is null, aborting!") - findNavController().navigateUp() - return - } - - viewModel = callLogGroup.lastCallLogViewModel - binding.viewModel = viewModel - if (viewModel.relatedCallLogs.value.orEmpty().isEmpty()) { - viewModel.addRelatedCallLogs(callLogGroup.callLogs) - } - - useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false - - binding.setNewContactClickListener { - val copy = viewModel.callLog.remoteAddress.clone() - copy.clean() - val address = copy.asStringUriOnly() - Log.i("[History] Creating contact with SIP URI [$address]") - sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event( - R.id.masterCallLogsFragment - ) - navigateToContacts(address) - } - - binding.setContactClickListener { - sharedViewModel.updateContactsAnimationsBasedOnDestination.value = Event( - R.id.masterCallLogsFragment - ) - val contactId = viewModel.contact.value?.refKey - if (contactId != null) { - Log.i("[History] Displaying native contact [$contactId]") - navigateToNativeContact(contactId) - } else { - val copy = viewModel.callLog.remoteAddress.clone() - copy.clean() - val address = copy.asStringUriOnly() - Log.i("[History] Displaying friend with address [$address]") - navigateToFriend(address) - } - } - - viewModel.startCallEvent.observe( - viewLifecycleOwner - ) { - it.consume { callLog -> - // To remove the GRUU if any - val address = callLog.remoteAddress.clone() - address.clean() - - if (coreContext.core.callsNb > 0) { - Log.i( - "[History] Starting dialer with pre-filled URI [${address.asStringUriOnly()}], is transfer? ${sharedViewModel.pendingCallTransfer}" - ) - sharedViewModel.updateDialerAnimationsBasedOnDestination.value = - Event(R.id.masterCallLogsFragment) - - val args = Bundle() - args.putString("URI", address.asStringUriOnly()) - args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) - args.putBoolean( - "SkipAutoCallStart", - true - ) // If auto start call setting is enabled, ignore it - navigateToDialer(args) - } else { - val localAddress = callLog.localAddress - Log.i( - "[History] Starting call to ${address.asStringUriOnly()} with local address ${localAddress.asStringUriOnly()}" - ) - coreContext.startCall(address, localAddress = localAddress) - } - } - } - - viewModel.chatRoomCreatedEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatRoom -> - val args = Bundle() - args.putString("LocalSipUri", chatRoom.localAddress.asStringUriOnly()) - args.putString("RemoteSipUri", chatRoom.peerAddress.asStringUriOnly()) - navigateToChatRoom(args) - } - } - - viewModel.onMessageToNotifyEvent.observe( - viewLifecycleOwner - ) { - it.consume { messageResourceId -> - (activity as MainActivity).showSnackBar(messageResourceId) - } - } - } - - override fun onResume() { - super.onResume() - viewModel.enableListener(true) - } - - override fun onPause() { - viewModel.enableListener(false) - super.onPause() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailConferenceCallLogFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/DetailConferenceCallLogFragment.kt deleted file mode 100644 index 984a8eac7..000000000 --- a/app/src/main/java/org/linphone/activities/main/history/fragments/DetailConferenceCallLogFragment.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.history.fragments - -import android.os.Bundle -import android.view.View -import androidx.navigation.fragment.findNavController -import org.linphone.R -import org.linphone.activities.* -import org.linphone.activities.main.* -import org.linphone.activities.main.history.viewmodels.CallLogViewModel -import org.linphone.core.tools.Log -import org.linphone.databinding.HistoryConfDetailFragmentBinding - -class DetailConferenceCallLogFragment : GenericFragment() { - private lateinit var viewModel: CallLogViewModel - - override fun getLayoutId(): Int = R.layout.history_conf_detail_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.sharedMainViewModel = sharedViewModel - - val callLogGroup = sharedViewModel.selectedCallLogGroup.value - if (callLogGroup == null) { - Log.e("[History] Call log group is null, aborting!") - findNavController().navigateUp() - return - } - - viewModel = callLogGroup.lastCallLogViewModel - binding.viewModel = viewModel - viewModel.addRelatedCallLogs(callLogGroup.callLogs) - - useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false - - viewModel.onMessageToNotifyEvent.observe( - viewLifecycleOwner - ) { - it.consume { messageResourceId -> - (activity as MainActivity).showSnackBar(messageResourceId) - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt deleted file mode 100644 index c10985411..000000000 --- a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt +++ /dev/null @@ -1,314 +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 . - */ -package org.linphone.activities.main.history.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 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.* -import org.linphone.activities.clearDisplayedCallHistory -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.fragments.MasterFragment -import org.linphone.activities.main.history.adapters.CallLogsListAdapter -import org.linphone.activities.main.history.data.GroupedCallLogData -import org.linphone.activities.main.history.viewmodels.CallLogsListViewModel -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.main.viewmodels.TabsViewModel -import org.linphone.activities.navigateToCallHistory -import org.linphone.activities.navigateToConferenceCallHistory -import org.linphone.activities.navigateToDialer -import org.linphone.core.ConferenceInfo -import org.linphone.core.tools.Log -import org.linphone.databinding.HistoryMasterFragmentBinding -import org.linphone.utils.* - -class MasterCallLogsFragment : MasterFragment() { - override val dialogConfirmationMessageBeforeRemoval = R.plurals.history_delete_dialog - private lateinit var listViewModel: CallLogsListViewModel - - private val observer = object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { - if (positionStart == 0 && itemCount == 1) { - scrollToTop() - } - } - } - - override fun getLayoutId(): Int = R.layout.history_master_fragment - - override fun onDestroyView() { - binding.callLogsList.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, false) - reenterTransition = MaterialSharedAxis(axis, false) - returnTransition = MaterialSharedAxis(axis, true) - exitTransition = MaterialSharedAxis(axis, true) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - listViewModel = ViewModelProvider(this)[CallLogsListViewModel::class.java] - binding.viewModel = listViewModel - - /* Shared view model & sliding pane related */ - - setUpSlidingPane(binding.slidingPane) - - sharedViewModel.layoutChangedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable - if (binding.slidingPane.isSlideable) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.history_nav_container) as NavHostFragment - if (navHostFragment.navController.currentDestination?.id == R.id.emptyCallHistoryFragment) { - Log.i( - "[History] Foldable device has been folded, closing side pane with empty fragment" - ) - binding.slidingPane.closePane() - } - } - } - } - - /* End of shared view model & sliding pane related */ - - _adapter = CallLogsListAdapter(listSelectionViewModel, viewLifecycleOwner) - // SubmitList is done on a background thread - // We need this adapter data observer to know when to scroll - adapter.registerAdapterDataObserver(observer) - binding.callLogsList.setHasFixedSize(true) - binding.callLogsList.adapter = adapter - - binding.setEditClickListener { - listSelectionViewModel.isEditionEnabled.value = true - } - - val layoutManager = LinearLayoutManager(requireContext()) - binding.callLogsList.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) - ) - val swipeListener = object : RecyclerViewSwipeListener { - override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {} - - override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) { - val viewModel = DialogViewModel(getString(R.string.history_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("[History] Index is out of bound, can't delete call log") - } else { - viewModel.showCancelButton { - adapter.notifyItemChanged(index) - dialog.dismiss() - } - - viewModel.showDeleteButton( - { - val deletedCallGroup = adapter.currentList[index] - listViewModel.deleteCallLogGroup(deletedCallGroup) - if (!binding.slidingPane.isSlideable && - deletedCallGroup.lastCallLogId == sharedViewModel.selectedCallLogGroup.value?.lastCallLogId - ) { - Log.i( - "[History] Currently displayed history has been deleted, removing detail fragment" - ) - clearDisplayedCallHistory() - } - dialog.dismiss() - }, - getString(R.string.dialog_delete) - ) - } - - dialog.show() - } - } - RecyclerViewSwipeUtils(ItemTouchHelper.LEFT, swipeConfiguration, swipeListener) - .attachToRecyclerView(binding.callLogsList) - - // Divider between items - binding.callLogsList.addItemDecoration( - AppUtils.getDividerDecoration(requireContext(), layoutManager) - ) - - // Displays formatted date header - val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) - binding.callLogsList.addItemDecoration(headerItemDecoration) - - listViewModel.callLogs.observe( - viewLifecycleOwner - ) { callLogs -> - adapter.submitList(callLogs) - } - - listViewModel.contactsUpdatedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - adapter.notifyItemRangeChanged(0, adapter.itemCount) - } - } - - adapter.selectedCallLogEvent.observe( - viewLifecycleOwner - ) { - it.consume { callLog -> - sharedViewModel.selectedCallLogGroup.value = callLog - if (callLog.lastCallLog.wasConference()) { - navigateToConferenceCallHistory(binding.slidingPane) - } else { - navigateToCallHistory(binding.slidingPane) - } - } - } - - adapter.startCallToEvent.observe( - viewLifecycleOwner - ) { - it.consume { callLogGroup -> - val callLog = callLogGroup.lastCallLog - val conferenceInfo = callLog.conferenceInfo - when { - conferenceInfo != null -> { - if (conferenceInfo.state == ConferenceInfo.State.Cancelled) { - var snackRes = R.string.conference_scheduled_cancelled_by_organizer - - val organizer = conferenceInfo.organizer - if (organizer != null) { - val localAccount = coreContext.core.accountList.find { account -> - val address = account.params.identityAddress - address != null && organizer.weakEqual(address) - } - if (localAccount != null) { - snackRes = R.string.conference_scheduled_cancelled_by_me - } - } - - val activity = requireActivity() as MainActivity - activity.showSnackBar(snackRes) - } else { - navigateToConferenceWaitingRoom( - conferenceInfo.uri?.asStringUriOnly().orEmpty(), - conferenceInfo.subject - ) - } - } - coreContext.core.callsNb > 0 -> { - val cleanAddress = LinphoneUtils.getCleanedAddress(callLog.remoteAddress) - Log.i( - "[History] Starting dialer with pre-filled URI ${cleanAddress.asStringUriOnly()}, is transfer? ${sharedViewModel.pendingCallTransfer}" - ) - sharedViewModel.updateDialerAnimationsBasedOnDestination.value = Event( - R.id.masterCallLogsFragment - ) - val args = Bundle() - args.putString("URI", cleanAddress.asStringUriOnly()) - args.putBoolean("Transfer", sharedViewModel.pendingCallTransfer) - args.putBoolean("SkipAutoCallStart", true) // If auto start call setting is enabled, ignore it - navigateToDialer(args) - } - else -> { - val cleanAddress = LinphoneUtils.getCleanedAddress(callLog.remoteAddress) - val localAddress = callLogGroup.lastCallLog.localAddress - Log.i( - "[History] Starting call to ${cleanAddress.asStringUriOnly()} with local address ${localAddress.asStringUriOnly()}" - ) - coreContext.startCall(cleanAddress, localAddress = localAddress) - } - } - } - } - - coreContext.core.resetMissedCallsCount() - coreContext.notificationsManager.dismissMissedCallNotification() - } - - override fun onResume() { - super.onResume() - - val tabsViewModel = requireActivity().run { - ViewModelProvider(this)[TabsViewModel::class.java] - } - tabsViewModel.updateMissedCallCount() - } - - override fun deleteItems(indexesOfItemToDelete: ArrayList) { - val list = ArrayList() - var closeSlidingPane = false - for (index in indexesOfItemToDelete) { - val callLogGroup = adapter.currentList[index] - list.add(callLogGroup) - - if (callLogGroup.lastCallLogId == sharedViewModel.selectedCallLogGroup.value?.lastCallLogId) { - closeSlidingPane = true - } - } - listViewModel.deleteCallLogGroups(list) - - if (!binding.slidingPane.isSlideable && closeSlidingPane) { - Log.i( - "[History] Currently displayed history has been deleted, removing detail fragment" - ) - clearDisplayedCallHistory() - } - } - - private fun scrollToTop() { - binding.callLogsList.scrollToPosition(0) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt deleted file mode 100644 index a5d1b74cb..000000000 --- a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogViewModel.kt +++ /dev/null @@ -1,260 +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 . - */ -package org.linphone.activities.main.history.viewmodels - -import androidx.lifecycle.MutableLiveData -import java.text.SimpleDateFormat -import java.util.* -import kotlin.collections.ArrayList -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.conference.data.ConferenceSchedulingParticipantData -import org.linphone.contact.GenericContactViewModel -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 CallLogViewModel(val callLog: CallLog, private val isRelated: Boolean = false) : GenericContactViewModel( - callLog.remoteAddress -) { - val peerSipUri: String by lazy { - LinphoneUtils.getDisplayableAddress(callLog.remoteAddress) - } - - val statusIconResource: Int by lazy { - if (callLog.dir == Call.Dir.Incoming) { - if (LinphoneUtils.isCallLogMissed(callLog)) { - R.drawable.call_status_missed - } else { - R.drawable.call_status_incoming - } - } else { - R.drawable.call_status_outgoing - } - } - - val iconContentDescription: Int by lazy { - if (callLog.dir == Call.Dir.Incoming) { - if (LinphoneUtils.isCallLogMissed(callLog)) { - R.string.content_description_missed_call - } else { - R.string.content_description_incoming_call - } - } else { - R.string.content_description_outgoing_call - } - } - - val directionIconResource: Int by lazy { - if (callLog.dir == Call.Dir.Incoming) { - if (LinphoneUtils.isCallLogMissed(callLog)) { - R.drawable.call_missed - } else { - R.drawable.call_incoming - } - } else { - R.drawable.call_outgoing - } - } - - val duration: String by lazy { - val dateFormat = SimpleDateFormat( - if (callLog.duration >= 3600) "HH:mm:ss" else "mm:ss", - Locale.getDefault() - ) - val cal = Calendar.getInstance() - cal[0, 0, 0, 0, 0] = callLog.duration - dateFormat.format(cal.time) - } - - val date: String by lazy { - TimestampUtils.toString(callLog.startDate, shortDate = false, hideYear = false) - } - - val startCallEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val chatRoomCreatedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val waitForChatRoomCreation = MutableLiveData() - - val chatAllowed = !corePreferences.disableChat - - val hidePlainChat = corePreferences.forceEndToEndEncryptedChat - - val secureChatAllowed = LinphoneUtils.isEndToEndEncryptedChatAvailable() && ( - corePreferences.allowEndToEndEncryptedChatWithoutPresence || ( - contact.value?.getPresenceModelForUriOrTel( - peerSipUri - )?.hasCapability(Friend.Capability.LimeX3Dh) ?: false - ) - ) - - val relatedCallLogs = MutableLiveData>() - - private val listener = object : CoreListenerStub() { - override fun onCallLogUpdated(core: Core, log: CallLog) { - if (callLog.remoteAddress.weakEqual(log.remoteAddress) && callLog.localAddress.weakEqual( - log.localAddress - ) - ) { - Log.i( - "[History Detail] New call log for ${callLog.remoteAddress.asStringUriOnly()} with local address ${callLog.localAddress.asStringUriOnly()}" - ) - addRelatedCallLogs(arrayListOf(log)) - } - } - } - - val isConferenceCallLog = callLog.wasConference() - - val conferenceSubject = callLog.conferenceInfo?.subject - val conferenceParticipantsData = MutableLiveData>() - val organizerParticipantData = MutableLiveData() - val conferenceTime = MutableLiveData() - val conferenceDate = MutableLiveData() - - val readOnlyNativeAddressBook = MutableLiveData() - - override val showGroupChatAvatar: Boolean - get() = isConferenceCallLog - - private val chatRoomListener = object : ChatRoomListenerStub() { - override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) { - if (state == ChatRoom.State.Created) { - waitForChatRoomCreation.value = false - chatRoomCreatedEvent.value = Event(chatRoom) - } else if (state == ChatRoom.State.CreationFailed) { - Log.e("[History Detail] Group chat room creation has failed !") - waitForChatRoomCreation.value = false - onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) - } - } - } - - init { - waitForChatRoomCreation.value = false - readOnlyNativeAddressBook.value = corePreferences.readOnlyNativeContacts - - if (!isRelated) { - val conferenceInfo = callLog.conferenceInfo - if (conferenceInfo != null) { - conferenceTime.value = TimestampUtils.timeToString(conferenceInfo.dateTime) - conferenceDate.value = if (TimestampUtils.isToday(conferenceInfo.dateTime)) { - AppUtils.getString(R.string.today) - } else { - TimestampUtils.toString( - conferenceInfo.dateTime, - onlyDate = true, - shortDate = false, - hideYear = false - ) - } - val organizer = conferenceInfo.organizer - if (organizer != null) { - organizerParticipantData.value = - ConferenceSchedulingParticipantData( - organizer, - showLimeBadge = false, - showDivider = false - ) - } - val list = arrayListOf() - for (participant in conferenceInfo.participants) { - list.add( - ConferenceSchedulingParticipantData( - participant, - showLimeBadge = false, - showDivider = true - ) - ) - } - conferenceParticipantsData.value = list - } - } - } - - override fun onCleared() { - destroy() - super.onCleared() - } - - fun destroy() { - if (!isRelated) { - relatedCallLogs.value.orEmpty().forEach(CallLogViewModel::destroy) - organizerParticipantData.value?.destroy() - conferenceParticipantsData.value.orEmpty() - .forEach(ConferenceSchedulingParticipantData::destroy) - } - } - - fun startCall() { - startCallEvent.value = Event(callLog) - } - - fun startChat(isSecured: Boolean) { - waitForChatRoomCreation.value = true - val chatRoom = LinphoneUtils.createOneToOneChatRoom(callLog.remoteAddress, isSecured) - - if (chatRoom != null) { - val state = chatRoom.state - Log.i("[History Detail] Found existing chat room in state $state") - if (state == ChatRoom.State.Created || state == ChatRoom.State.Terminated) { - waitForChatRoomCreation.value = false - chatRoomCreatedEvent.value = Event(chatRoom) - } else { - chatRoom.addListener(chatRoomListener) - } - } else { - waitForChatRoomCreation.value = false - Log.e( - "[History Detail] Couldn't create chat room with address ${callLog.remoteAddress}" - ) - onMessageToNotifyEvent.value = Event(R.string.chat_room_creation_failed_snack) - } - } - - fun addRelatedCallLogs(callLogs: ArrayList) { - val list = arrayListOf() - - // We assume new logs are more recent than the ones we already have, so we add them first - for (callLog in callLogs) { - list.add(CallLogViewModel(callLog, true)) - } - list.addAll(relatedCallLogs.value.orEmpty()) - - relatedCallLogs.value = list - } - - fun enableListener(enable: Boolean) { - if (enable) { - coreContext.core.addListener(listener) - } else { - coreContext.core.removeListener(listener) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt b/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt deleted file mode 100644 index cc41770cc..000000000 --- a/app/src/main/java/org/linphone/activities/main/history/viewmodels/CallLogsListViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.history.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.main.history.data.GroupedCallLogData -import org.linphone.contact.ContactsUpdatedListenerStub -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.Event -import org.linphone.utils.LinphoneUtils -import org.linphone.utils.TimestampUtils - -class CallLogsListViewModel : ViewModel() { - val callLogs = MutableLiveData>() - - val filter = MutableLiveData() - - val showConferencesFilter = MutableLiveData() - - val contactsUpdatedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onCallLogUpdated(core: Core, log: CallLog) { - updateCallLogs() - } - } - - private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { - override fun onContactsUpdated() { - Log.i("[Call Logs] Contacts have changed") - contactsUpdatedEvent.value = Event(true) - } - } - - init { - filter.value = CallLogsFilter.ALL - updateCallLogs() - - showConferencesFilter.value = LinphoneUtils.isRemoteConferencingAvailable() - - coreContext.core.addListener(listener) - coreContext.contactsManager.addListener(contactsUpdatedListener) - } - - override fun onCleared() { - callLogs.value.orEmpty().forEach(GroupedCallLogData::destroy) - - coreContext.contactsManager.removeListener(contactsUpdatedListener) - coreContext.core.removeListener(listener) - - super.onCleared() - } - - fun showAllCallLogs() { - filter.value = CallLogsFilter.ALL - updateCallLogs() - } - - fun showOnlyMissedCallLogs() { - filter.value = CallLogsFilter.MISSED - updateCallLogs() - } - - fun showOnlyConferenceCallLogs() { - filter.value = CallLogsFilter.CONFERENCE - updateCallLogs() - } - - fun deleteCallLogGroup(callLog: GroupedCallLogData?) { - if (callLog != null) { - for (log in callLog.callLogs) { - coreContext.core.removeCallLog(log) - } - } - - updateCallLogs() - } - - fun deleteCallLogGroups(listToDelete: ArrayList) { - for (callLog in listToDelete) { - for (log in callLog.callLogs) { - coreContext.core.removeCallLog(log) - } - } - - updateCallLogs() - } - - private fun computeCallLogs(callLogs: Array, missed: Boolean, conference: Boolean): ArrayList { - var previousCallLogGroup: GroupedCallLogData? = null - val list = arrayListOf() - - for (callLog in callLogs) { - if ((!missed && !conference) || (missed && LinphoneUtils.isCallLogMissed(callLog)) || (conference && callLog.wasConference())) { - if (previousCallLogGroup == null) { - previousCallLogGroup = GroupedCallLogData(callLog) - } else if (!callLog.wasConference() && // Do not group conference call logs - callLog.wasConference() == previousCallLogGroup.lastCallLog.wasConference() && // Check that both are of the same type, if one has a conf-id and not the other the equal method will return true ! - previousCallLogGroup.lastCallLog.localAddress.weakEqual(callLog.localAddress) && - previousCallLogGroup.lastCallLog.remoteAddress.equal(callLog.remoteAddress) - ) { - if (TimestampUtils.isSameDay( - previousCallLogGroup.lastCallLogStartTimestamp, - callLog.startDate - ) - ) { - previousCallLogGroup.callLogs.add(callLog) - previousCallLogGroup.updateLastCallLog(callLog) - } else { - list.add(previousCallLogGroup) - previousCallLogGroup = GroupedCallLogData(callLog) - } - } else { - list.add(previousCallLogGroup) - previousCallLogGroup = GroupedCallLogData(callLog) - } - } - } - if (previousCallLogGroup != null && !list.contains(previousCallLogGroup)) { - list.add(previousCallLogGroup) - } - - return list - } - - private fun updateCallLogs() { - callLogs.value.orEmpty().forEach(GroupedCallLogData::destroy) - - val allCallLogs = coreContext.core.callLogs - Log.i("[Call Logs] ${allCallLogs.size} call logs found") - - callLogs.value = when (filter.value) { - CallLogsFilter.MISSED -> computeCallLogs(allCallLogs, missed = true, conference = false) - CallLogsFilter.CONFERENCE -> computeCallLogs( - allCallLogs, - missed = false, - conference = true - ) - else -> computeCallLogs(allCallLogs, missed = false, conference = false) - } - } -} - -enum class CallLogsFilter { - ALL, - MISSED, - CONFERENCE -} diff --git a/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt b/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt deleted file mode 100644 index 6151ba58a..000000000 --- a/app/src/main/java/org/linphone/activities/main/recordings/adapters/RecordingsListAdapter.kt +++ /dev/null @@ -1,163 +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 . - */ -package org.linphone.activities.main.recordings.adapters - -import android.content.Context -import android.view.LayoutInflater -import android.view.TextureView -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.RecyclerView -import org.linphone.R -import org.linphone.activities.main.adapters.SelectionListAdapter -import org.linphone.activities.main.recordings.data.RecordingData -import org.linphone.activities.main.viewmodels.ListTopBarViewModel -import org.linphone.databinding.GenericListHeaderBinding -import org.linphone.databinding.RecordingListCellBinding -import org.linphone.utils.* - -class RecordingsListAdapter( - selectionVM: ListTopBarViewModel, - private val viewLifecycleOwner: LifecycleOwner -) : SelectionListAdapter( - selectionVM, - RecordingDiffCallback() -), - HeaderAdapter { - - private lateinit var videoSurface: TextureView - - fun setVideoTextureView(textureView: TextureView) { - videoSurface = textureView - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val binding: RecordingListCellBinding = DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.recording_list_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: RecordingListCellBinding - ) : RecyclerView.ViewHolder(binding.root) { - fun bind(recording: RecordingData) { - with(binding) { - data = recording - - lifecycleOwner = viewLifecycleOwner - - // This is for item selection through ListTopBarFragment - position = bindingAdapterPosition - selectionListViewModel = selectionViewModel - - setClickListener { - if (selectionViewModel.isEditionEnabled.value == true) { - selectionViewModel.onToggleSelect(bindingAdapterPosition) - } - } - - setPlayListener { - if (recording.isPlaying.value == true) { - recording.pause() - } else { - recording.play() - if (recording.isVideoAvailable()) { - recording.setTextureView(videoSurface) - } - } - } - - executePendingBindings() - } - } - } - - override fun displayHeaderForPosition(position: Int): Boolean { - if (position >= itemCount) return false - - val recording = getItem(position) - val date = recording.date - val previousPosition = position - 1 - - return if (previousPosition >= 0) { - val previousItemDate = getItem(previousPosition).date - !TimestampUtils.isSameDay(date, previousItemDate) - } else { - true - } - } - - override fun getHeaderViewForPosition(context: Context, position: Int): View { - val recording = getItem(position) - val date = formatDate(context, recording.date.time) - val binding: GenericListHeaderBinding = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.generic_list_header, - null, - false - ) - binding.title = date - binding.executePendingBindings() - return binding.root - } - - private fun formatDate(context: Context, date: Long): String { - // Recordings is one of the few items in Linphone that is already in milliseconds - if (TimestampUtils.isToday(date, false)) { - return context.getString(R.string.today) - } else if (TimestampUtils.isYesterday(date, false)) { - return context.getString(R.string.yesterday) - } - return TimestampUtils.toString( - date, - onlyDate = true, - timestampInSecs = false, - shortDate = false, - hideYear = false - ) - } -} - -private class RecordingDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: RecordingData, - newItem: RecordingData - ): Boolean { - return oldItem.compareTo(newItem) == 0 - } - - override fun areContentsTheSame( - oldItem: RecordingData, - newItem: RecordingData - ): Boolean { - return false // for headers - } -} diff --git a/app/src/main/java/org/linphone/activities/main/recordings/data/RecordingData.kt b/app/src/main/java/org/linphone/activities/main/recordings/data/RecordingData.kt deleted file mode 100644 index 022ffd171..000000000 --- a/app/src/main/java/org/linphone/activities/main/recordings/data/RecordingData.kt +++ /dev/null @@ -1,214 +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 . - */ -package org.linphone.activities.main.recordings.data - -import android.graphics.SurfaceTexture -import android.view.TextureView -import androidx.lifecycle.MutableLiveData -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* -import java.util.regex.Pattern -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.ticker -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.Player -import org.linphone.core.PlayerListener -import org.linphone.core.tools.Log -import org.linphone.utils.AudioRouteUtils -import org.linphone.utils.LinphoneUtils - -class RecordingData(val path: String, private val recordingListener: RecordingListener) : Comparable { - companion object { - val RECORD_PATTERN: Pattern = - Pattern.compile(".*/(.*)_(\\d{2}-\\d{2}-\\d{4}-\\d{2}-\\d{2}-\\d{2})\\..*") - } - - lateinit var name: String - lateinit var date: Date - - val duration = MutableLiveData() - val formattedDuration = MutableLiveData() - val formattedDate = MutableLiveData() - val position = MutableLiveData() - val formattedPosition = MutableLiveData() - val isPlaying = MutableLiveData() - - private val tickerChannel = ticker(1000, 1000) - - private lateinit var player: Player - private val listener = PlayerListener { - Log.i("[Recording] End of file reached") - stop() - recordingListener.onPlayingEnded() - } - - private val textureViewListener = object : TextureView.SurfaceTextureListener { - override fun onSurfaceTextureSizeChanged( - surface: SurfaceTexture, - width: Int, - height: Int - ) { } - - override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { } - - override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { - player.setWindowId(null) - return true - } - - override fun onSurfaceTextureAvailable( - surface: SurfaceTexture, - width: Int, - height: Int - ) { - Log.i("[Recording] Surface texture should be available now") - player.setWindowId(surface) - } - } - - private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - - init { - val m = RECORD_PATTERN.matcher(path) - if (m.matches() && m.groupCount() >= 2) { - name = m.group(1) - date = LinphoneUtils.getRecordingDateFromFileName(m.group(2)) - } - isPlaying.value = false - - position.value = 0 - formattedPosition.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format(0) - - initPlayer() - } - - override fun compareTo(other: RecordingData): Int { - return -date.compareTo(other.date) - } - - fun destroy() { - scope.cancel() - tickerChannel.cancel() - - player.setWindowId(null) - if (!isClosed()) player.close() - - player.removeListener(listener) - } - - fun play() { - if (isClosed()) { - player.open(path) - player.seek(0) - } - - player.start() - isPlaying.value = true - recordingListener.onPlayingStarted(isVideoAvailable()) - - scope.launch { - withContext(Dispatchers.IO) { - for (tick in tickerChannel) { - withContext(Dispatchers.Main) { - if (player.state == Player.State.Playing) { - updatePosition() - } - } - } - } - } - } - - fun isVideoAvailable(): Boolean { - return player.isVideoAvailable - } - - fun pause() { - player.pause() - isPlaying.value = false - } - - fun onProgressChanged(progress: Any) { - if (progress is Int) { - if (player.state == Player.State.Playing) { - pause() - } - player.seek(progress) - updatePosition() - } - } - - fun setTextureView(textureView: TextureView) { - Log.i("[Recording] Is TextureView available? ${textureView.isAvailable}") - if (textureView.isAvailable) { - player.setWindowId(textureView.surfaceTexture) - } else { - textureView.surfaceTextureListener = textureViewListener - } - } - - fun export() { - recordingListener.onExportClicked(path) - } - - private fun initPlayer() { - val playbackSoundCard = AudioRouteUtils.getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage() - Log.i("[Recording] Using device $playbackSoundCard to make the call recording playback") - - val localPlayer = coreContext.core.createLocalPlayer(playbackSoundCard, null, null) - if (localPlayer != null) { - player = localPlayer - } else { - Log.e("[Recording] Couldn't create local player!") - } - player.addListener(listener) - - player.open(path) - duration.value = player.duration - formattedDuration.value = SimpleDateFormat("mm:ss", Locale.getDefault()).format( - player.duration - ) // is already in milliseconds - formattedDate.value = DateFormat.getTimeInstance(DateFormat.SHORT).format(date) - } - - private fun updatePosition() { - val progress = if (isClosed()) 0 else player.currentPosition - position.postValue(progress) - formattedPosition.postValue(SimpleDateFormat("mm:ss", Locale.getDefault()).format(progress)) // is already in milliseconds - } - - private fun stop() { - pause() - player.seek(0) - updatePosition() - player.close() - } - - private fun isClosed(): Boolean { - return player.state == Player.State.Closed - } - - interface RecordingListener { - fun onPlayingStarted(videoAvailable: Boolean) - fun onPlayingEnded() - fun onExportClicked(path: String) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt b/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt deleted file mode 100644 index b8c48a8a0..000000000 --- a/app/src/main/java/org/linphone/activities/main/recordings/fragments/RecordingsFragment.kt +++ /dev/null @@ -1,149 +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 . - */ -package org.linphone.activities.main.recordings.fragments - -import android.content.ActivityNotFoundException -import android.content.Intent -import android.os.Bundle -import android.view.MotionEvent -import android.view.View -import androidx.lifecycle.ViewModelProvider -import androidx.recyclerview.widget.LinearLayoutManager -import org.linphone.R -import org.linphone.activities.main.fragments.MasterFragment -import org.linphone.activities.main.recordings.adapters.RecordingsListAdapter -import org.linphone.activities.main.recordings.data.RecordingData -import org.linphone.activities.main.recordings.viewmodels.RecordingsViewModel -import org.linphone.core.tools.Log -import org.linphone.databinding.RecordingsFragmentBinding -import org.linphone.utils.AppUtils -import org.linphone.utils.FileUtils -import org.linphone.utils.RecyclerViewHeaderDecoration - -class RecordingsFragment : MasterFragment() { - private lateinit var viewModel: RecordingsViewModel - - private var videoX: Float = 0f - private var videoY: Float = 0f - - override fun getLayoutId(): Int = R.layout.recordings_fragment - - override fun onDestroyView() { - binding.recordingsList.adapter = null - super.onDestroyView() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - viewModel = ViewModelProvider(this)[RecordingsViewModel::class.java] - binding.viewModel = viewModel - - _adapter = RecordingsListAdapter(listSelectionViewModel, viewLifecycleOwner) - binding.recordingsList.setHasFixedSize(true) - binding.recordingsList.adapter = adapter - - val layoutManager = LinearLayoutManager(requireContext()) - binding.recordingsList.layoutManager = layoutManager - - // Divider between items - binding.recordingsList.addItemDecoration( - AppUtils.getDividerDecoration(requireContext(), layoutManager) - ) - - // Displays the first letter header - val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter) - binding.recordingsList.addItemDecoration(headerItemDecoration) - - viewModel.recordingsList.observe( - viewLifecycleOwner - ) { recordings -> - adapter.submitList(recordings) - } - - viewModel.exportRecordingEvent.observe( - viewLifecycleOwner - ) { - it.consume { path -> - val publicFilePath = FileUtils.getPublicFilePath(requireContext(), "file://$path") - Log.i("[Recordings] Exporting file [$path] with public URI [$publicFilePath]") - val intent = Intent(Intent.ACTION_SEND) - intent.type = " video/x-matroska" - intent.putExtra(Intent.EXTRA_STREAM, publicFilePath) - intent.putExtra(Intent.EXTRA_TEXT, getString(R.string.recordings_export)) - - try { - requireActivity().startActivity( - Intent.createChooser(intent, getString(R.string.recordings_export)) - ) - } catch (anfe: ActivityNotFoundException) { - Log.e(anfe) - } - } - } - - binding.setEditClickListener { listSelectionViewModel.isEditionEnabled.value = true } - - binding.setVideoTouchListener { v, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - videoX = v.x - event.rawX - videoY = v.y - event.rawY - } - MotionEvent.ACTION_MOVE -> { - v.animate() - .x(event.rawX + videoX) - .y(event.rawY + videoY) - .setDuration(0) - .start() - } - else -> { - v.performClick() - false - } - } - true - } - - adapter.setVideoTextureView(binding.recordingVideoSurface) - } - - override fun deleteItems(indexesOfItemToDelete: ArrayList) { - val list = ArrayList() - for (index in indexesOfItemToDelete) { - val recording = adapter.currentList[index] - list.add(recording) - } - viewModel.deleteRecordings(list) - } - - override fun onResume() { - if (this::viewModel.isInitialized) { - viewModel.updateRecordingsList() - } else { - Log.e( - "[Recordings] Fragment resuming but viewModel lateinit property isn't initialized!" - ) - } - super.onResume() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingsViewModel.kt deleted file mode 100644 index cc12b4442..000000000 --- a/app/src/main/java/org/linphone/activities/main/recordings/viewmodels/RecordingsViewModel.kt +++ /dev/null @@ -1,120 +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 . - */ -package org.linphone.activities.main.recordings.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.media.AudioFocusRequestCompat -import kotlin.collections.ArrayList -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.main.recordings.data.RecordingData -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils -import org.linphone.utils.Event -import org.linphone.utils.FileUtils - -class RecordingsViewModel : ViewModel() { - val recordingsList = MutableLiveData>() - - val isVideoVisible = MutableLiveData() - - val exportRecordingEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private var recordingPlayingAudioFocusRequest: AudioFocusRequestCompat? = null - - private val recordingListener = object : RecordingData.RecordingListener { - override fun onPlayingStarted(videoAvailable: Boolean) { - if (recordingPlayingAudioFocusRequest == null) { - recordingPlayingAudioFocusRequest = AppUtils.acquireAudioFocusForVoiceRecordingOrPlayback( - coreContext.context - ) - } - - isVideoVisible.value = videoAvailable - } - - override fun onPlayingEnded() { - val request = recordingPlayingAudioFocusRequest - if (request != null) { - AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request) - recordingPlayingAudioFocusRequest = null - } - - isVideoVisible.value = false - } - - override fun onExportClicked(path: String) { - exportRecordingEvent.value = Event(path) - } - } - - init { - isVideoVisible.value = false - } - - override fun onCleared() { - recordingsList.value.orEmpty().forEach(RecordingData::destroy) - - val request = recordingPlayingAudioFocusRequest - if (request != null) { - AppUtils.releaseAudioFocusForVoiceRecordingOrPlayback(coreContext.context, request) - recordingPlayingAudioFocusRequest = null - } - - super.onCleared() - } - - fun deleteRecordings(list: ArrayList) { - for (recording in list) { - // Hide video when removing a recording being played with video. - if (recording.isPlaying.value == true && recording.isVideoAvailable()) { - isVideoVisible.value = false - } - - Log.i("[Recordings] Deleting recording ${recording.path}") - FileUtils.deleteFile(recording.path) - } - - updateRecordingsList() - } - - fun updateRecordingsList() { - recordingsList.value.orEmpty().forEach(RecordingData::destroy) - val list = arrayListOf() - - for (f in FileUtils.getFileStorageDir().listFiles().orEmpty()) { - Log.d("[Recordings] Found file ${f.path}") - if (RecordingData.RECORD_PATTERN.matcher(f.path).matches()) { - list.add( - RecordingData( - f.path, - recordingListener - ) - ) - Log.i("[Recordings] Found record ${f.path}") - } - } - - list.sort() - recordingsList.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/SettingListener.kt b/app/src/main/java/org/linphone/activities/main/settings/SettingListener.kt deleted file mode 100644 index 16708c9df..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/SettingListener.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings - -interface SettingListener { - fun onClicked() - - fun onAccountClicked(identity: String) - - fun onTextValueChanged(newValue: String) - - fun onBoolValueChanged(newValue: Boolean) - - fun onListValueChanged(position: Int) -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/SettingListenerStub.kt b/app/src/main/java/org/linphone/activities/main/settings/SettingListenerStub.kt deleted file mode 100644 index 2bb5fa46c..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/SettingListenerStub.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings - -open class SettingListenerStub : SettingListener { - override fun onClicked() {} - - override fun onAccountClicked(identity: String) {} - - override fun onTextValueChanged(newValue: String) {} - - override fun onBoolValueChanged(newValue: Boolean) {} - - override fun onListValueChanged(position: Int) {} -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt deleted file mode 100644 index b06648585..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/AccountSettingsFragment.kt +++ /dev/null @@ -1,150 +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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.app.Dialog -import android.os.Bundle -import android.view.View -import androidx.core.view.doOnPreDraw -import androidx.lifecycle.ViewModelProvider -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.settings.viewmodels.AccountSettingsViewModel -import org.linphone.activities.main.settings.viewmodels.AccountSettingsViewModelFactory -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.navigateToPhoneLinking -import org.linphone.core.tools.Log -import org.linphone.databinding.SettingsAccountFragmentBinding -import org.linphone.utils.DialogUtils -import org.linphone.utils.Event - -class AccountSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: AccountSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_account_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - val identity = arguments?.getString("Identity") - if (identity == null) { - Log.e("[Account Settings] Identity is null, aborting!") - goBack() - return - } - - try { - viewModel = ViewModelProvider(this, AccountSettingsViewModelFactory(identity))[AccountSettingsViewModel::class.java] - } catch (nsee: NoSuchElementException) { - Log.e("[Account Settings] Failed to find Account object, aborting!") - goBack() - return - } - binding.viewModel = viewModel - - viewModel.linkPhoneNumberEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val authInfo = viewModel.account.findAuthInfo() - if (authInfo == null) { - Log.e( - "[Account Settings] Failed to find auth info for account ${viewModel.account}" - ) - } else { - val args = Bundle() - args.putString("Username", authInfo.username) - args.putString("Password", authInfo.password) - args.putString("HA1", authInfo.ha1) - navigateToPhoneLinking(args) - } - } - } - - viewModel.accountRemovedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - sharedViewModel.accountRemoved.value = true - goBack() - } - } - - viewModel.accountDefaultEvent.observe( - viewLifecycleOwner - ) { - it.consume { - sharedViewModel.defaultAccountChanged.value = true - } - } - - viewModel.deleteAccountRequiredEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val defaultDomainAccount = viewModel.account.params.identityAddress?.domain == corePreferences.defaultDomain - Log.i( - "[Account Settings] User clicked on delete account, showing confirmation dialog for ${if (defaultDomainAccount) "default domain account" else "third party account"}" - ) - val dialogViewModel = if (defaultDomainAccount) { - DialogViewModel( - getString( - R.string.account_setting_delete_sip_linphone_org_confirmation_dialog - ), - getString(R.string.account_setting_delete_dialog_title) - ) - } else { - DialogViewModel( - getString(R.string.account_setting_delete_generic_confirmation_dialog), - getString(R.string.account_setting_delete_dialog_title) - ) - } - val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) - - dialogViewModel.showIcon = true - dialogViewModel.iconResource = R.drawable.dialog_delete_icon - dialogViewModel.showSubscribeLinphoneOrgLink = defaultDomainAccount - - dialogViewModel.showCancelButton { - Log.i("[Account Settings] User cancelled account removal") - dialog.dismiss() - } - - dialogViewModel.showDeleteButton( - { - viewModel.startDeleteAccount() - dialog.dismiss() - }, - getString(R.string.dialog_delete) - ) - - dialog.show() - } - } - - view.doOnPreDraw { - // Notifies fragment is ready to be drawn - sharedViewModel.accountSettingsFragmentOpenedEvent.value = Event(true) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AdvancedSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AdvancedSettingsFragment.kt deleted file mode 100644 index bfe786392..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/AdvancedSettingsFragment.kt +++ /dev/null @@ -1,147 +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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.content.* -import android.net.Uri -import android.os.Bundle -import android.provider.Settings -import android.view.View -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModelProvider -import org.linphone.R -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.settings.viewmodels.AdvancedSettingsViewModel -import org.linphone.core.tools.Log -import org.linphone.core.tools.compatibility.DeviceUtils -import org.linphone.databinding.SettingsAdvancedFragmentBinding -import org.linphone.utils.AppUtils -import org.linphone.utils.PowerManagerUtils - -class AdvancedSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: AdvancedSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_advanced_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - viewModel = ViewModelProvider(this)[AdvancedSettingsViewModel::class.java] - binding.viewModel = viewModel - - viewModel.uploadFinishedEvent.observe( - viewLifecycleOwner - ) { - it.consume { url -> - val clipboard = - requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Logs url", url) - clipboard.setPrimaryClip(clip) - - val activity = requireActivity() as MainActivity - activity.showSnackBar(R.string.logs_url_copied_to_clipboard) - - AppUtils.shareUploadedLogsUrl(activity, url) - } - } - - viewModel.uploadErrorEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val activity = requireActivity() as MainActivity - activity.showSnackBar(R.string.logs_upload_failure) - } - } - - viewModel.resetCompleteEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val activity = requireActivity() as MainActivity - activity.showSnackBar(R.string.logs_reset_complete) - } - } - - viewModel.setNightModeEvent.observe( - viewLifecycleOwner - ) { - it.consume { value -> - AppCompatDelegate.setDefaultNightMode( - when (value) { - 0 -> AppCompatDelegate.MODE_NIGHT_NO - 1 -> AppCompatDelegate.MODE_NIGHT_YES - else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM - } - ) - } - } - - viewModel.backgroundModeEnabled.value = !DeviceUtils.isAppUserRestricted(requireContext()) - - viewModel.goToBatterySettingsEvent.observe( - viewLifecycleOwner - ) { - it.consume { - try { - val intent = Intent("android.settings.IGNORE_BATTERY_OPTIMIZATION_SETTINGS") - startActivity(intent) - } catch (anfe: ActivityNotFoundException) { - Log.e("[Advanced Settings] ActivityNotFound exception: ", anfe) - } - } - } - - viewModel.powerManagerSettingsVisibility.value = PowerManagerUtils.getDevicePowerManagerIntent( - requireContext() - ) != null - viewModel.goToPowerManagerSettingsEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val intent = PowerManagerUtils.getDevicePowerManagerIntent(requireActivity()) - if (intent != null) { - try { - startActivity(intent) - } catch (se: SecurityException) { - Log.e("[Advanced Settings] Security exception: ", se) - } - } - } - } - - viewModel.goToAndroidSettingsEvent.observe( - viewLifecycleOwner - ) { - it.consume { - val intent = Intent() - intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS - intent.addCategory(Intent.CATEGORY_DEFAULT) - intent.data = Uri.parse("package:${requireContext().packageName}") - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - ContextCompat.startActivity(requireContext(), intent, null) - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt deleted file mode 100644 index 78e2e2cf2..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/AudioSettingsFragment.kt +++ /dev/null @@ -1,124 +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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import androidx.databinding.DataBindingUtil -import androidx.databinding.ViewDataBinding -import androidx.lifecycle.ViewModelProvider -import org.linphone.BR -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.activities.main.settings.viewmodels.AudioSettingsViewModel -import org.linphone.core.tools.Log -import org.linphone.databinding.SettingsAudioFragmentBinding -import org.linphone.utils.PermissionHelper - -class AudioSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: AudioSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_audio_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - viewModel = ViewModelProvider(this)[AudioSettingsViewModel::class.java] - binding.viewModel = viewModel - - viewModel.askAudioRecordPermissionForEchoCancellerCalibrationEvent.observe( - viewLifecycleOwner - ) { - it.consume { - Log.i( - "[Audio Settings] Asking for RECORD_AUDIO permission for echo canceller calibration" - ) - requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 1) - } - } - - viewModel.askAudioRecordPermissionForEchoTesterEvent.observe( - viewLifecycleOwner - ) { - it.consume { - Log.i("[Audio Settings] Asking for RECORD_AUDIO permission for echo tester") - requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 2) - } - } - - initAudioCodecsList() - - if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) { - Log.i("[Audio Settings] Asking for RECORD_AUDIO permission") - requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 0) - } - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED - if (granted) { - Log.i("[Audio Settings] RECORD_AUDIO permission granted") - if (requestCode == 1) { - viewModel.startEchoCancellerCalibration() - } else if (requestCode == 2) { - viewModel.startEchoTester() - } - } else { - Log.w("[Audio Settings] RECORD_AUDIO permission denied") - } - } - - private fun initAudioCodecsList() { - val list = arrayListOf() - for (payload in coreContext.core.audioPayloadTypes) { - val binding = DataBindingUtil.inflate( - LayoutInflater.from(requireContext()), - R.layout.settings_widget_switch, - null, - false - ) - binding.setVariable(BR.title, payload.mimeType) - binding.setVariable(BR.subtitle, "${payload.clockRate} Hz") - binding.setVariable(BR.checked, payload.enabled()) - binding.setVariable( - BR.listener, - object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - payload.enable(newValue) - } - } - ) - binding.lifecycleOwner = viewLifecycleOwner - list.add(binding) - } - viewModel.audioCodecs.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/CallSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/CallSettingsFragment.kt deleted file mode 100644 index 735febf15..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/CallSettingsFragment.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.content.Intent -import android.content.pm.PackageManager -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.provider.Settings -import android.view.View -import androidx.lifecycle.ViewModelProvider -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.settings.viewmodels.CallSettingsViewModel -import org.linphone.compatibility.Compatibility -import org.linphone.core.tools.Log -import org.linphone.databinding.SettingsCallFragmentBinding -import org.linphone.mediastream.Version -import org.linphone.telecom.TelecomHelper - -class CallSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: CallSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_call_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - viewModel = ViewModelProvider(this)[CallSettingsViewModel::class.java] - binding.viewModel = viewModel - - viewModel.systemWideOverlayEnabledEvent.observe( - viewLifecycleOwner - ) { - it.consume { - if (!Compatibility.canDrawOverlay(requireContext())) { - val intent = Intent( - "android.settings.action.MANAGE_OVERLAY_PERMISSION", - Uri.parse("package:${requireContext().packageName}") - ) - startActivityForResult(intent, 0) - } - } - } - - viewModel.goToAndroidNotificationSettingsEvent.observe( - viewLifecycleOwner - ) { - it.consume { - if (Build.VERSION.SDK_INT >= Version.API26_O_80) { - val i = Intent() - i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS - i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) - i.putExtra( - Settings.EXTRA_CHANNEL_ID, - getString(R.string.notification_channel_service_id) - ) - i.addCategory(Intent.CATEGORY_DEFAULT) - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - startActivity(i) - } - } - } - - viewModel.enableTelecomManagerEvent.observe( - viewLifecycleOwner - ) { - it.consume { - if (Compatibility.hasTelecomManagerFeature(requireContext())) { - if (!Compatibility.hasTelecomManagerPermissions(requireContext())) { - Compatibility.requestTelecomManagerPermissions(requireActivity(), 1) - } else if (!TelecomHelper.exists()) { - corePreferences.useTelecomManager = true - Log.w("[Telecom Helper] Doesn't exists yet, creating it") - TelecomHelper.create(requireContext()) - updateTelecomManagerAccount() - } - } else { - Log.e( - "[Telecom Helper] Telecom Helper can't be created, device doesn't support connection service!" - ) - } - } - } - - viewModel.goToAndroidNotificationSettingsEvent.observe( - viewLifecycleOwner - ) { - it.consume { - if (Build.VERSION.SDK_INT >= Version.API26_O_80) { - val i = Intent() - i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS - i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) - i.putExtra( - Settings.EXTRA_CHANNEL_ID, - getString(R.string.notification_channel_service_id) - ) - i.addCategory(Intent.CATEGORY_DEFAULT) - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - startActivity(i) - } - } - } - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - - if (requestCode == 0 && !Compatibility.canDrawOverlay(requireContext())) { - viewModel.overlayListener.onBoolValueChanged(false) - } else if (requestCode == 1) { - if (!TelecomHelper.exists()) { - Log.w("[Telecom Helper] Doesn't exists yet, creating it") - if (Compatibility.hasTelecomManagerFeature(requireContext())) { - TelecomHelper.create(requireContext()) - } else { - Log.e( - "[Telecom Helper] Telecom Helper can't be created, device doesn't support connection service" - ) - } - } - updateTelecomManagerAccount() - } - } - - private fun updateTelecomManagerAccount() { - if (!TelecomHelper.exists()) { - Log.e("[Telecom Helper] Doesn't exists, can't update account!") - return - } - // We have to refresh the account object otherwise isAccountEnabled will always return false... - val account = TelecomHelper.get().findExistingAccount(requireContext()) - TelecomHelper.get().updateAccount(account) - val enabled = TelecomHelper.get().isAccountEnabled() - Log.i("[Call Settings] Telecom Manager is ${if (enabled) "enabled" else "disabled"}") - viewModel.useTelecomManager.value = enabled - corePreferences.useTelecomManager = enabled - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - for (index in grantResults.indices) { - val result = grantResults[index] - if (result != PackageManager.PERMISSION_GRANTED) { - Log.w( - "[Call Settings] ${permissions[index]} permission denied but required for telecom manager" - ) - viewModel.useTelecomManager.value = false - corePreferences.useTelecomManager = false - return - } - } - - if (Compatibility.hasTelecomManagerFeature(requireContext())) { - TelecomHelper.create(requireContext()) - updateTelecomManagerAccount() - } else { - Log.e( - "[Telecom Helper] Telecom Helper can't be created, device doesn't support connection service" - ) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt deleted file mode 100644 index 23c6ef87e..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/ChatSettingsFragment.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.activities.main.settings.fragments - -import android.content.Intent -import android.os.Build -import android.os.Bundle -import android.provider.Settings -import android.view.View -import androidx.lifecycle.ViewModelProvider -import org.linphone.R -import org.linphone.activities.main.chat.viewmodels.ChatRoomsListViewModel -import org.linphone.activities.main.settings.viewmodels.ChatSettingsViewModel -import org.linphone.databinding.SettingsChatFragmentBinding -import org.linphone.mediastream.Version -import org.linphone.utils.ShortcutsHelper - -class ChatSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: ChatSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_chat_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - viewModel = ViewModelProvider(this)[ChatSettingsViewModel::class.java] - binding.viewModel = viewModel - - viewModel.launcherShortcutsEvent.observe( - viewLifecycleOwner - ) { - it.consume { newValue -> - if (newValue) { - ShortcutsHelper.createShortcutsToChatRooms(requireContext()) - } else { - ShortcutsHelper.removeShortcuts(requireContext()) - } - } - } - - viewModel.goToAndroidNotificationSettingsEvent.observe( - viewLifecycleOwner - ) { - it.consume { - if (Build.VERSION.SDK_INT >= Version.API26_O_80) { - val i = Intent() - i.action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS - i.putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().packageName) - i.putExtra( - Settings.EXTRA_CHANNEL_ID, - getString(R.string.notification_channel_chat_id) - ) - i.addCategory(Intent.CATEGORY_DEFAULT) - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - i.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) - startActivity(i) - } - } - } - - viewModel.reloadChatRoomsEvent.observe(viewLifecycleOwner) { - it.consume { - reloadChatRooms() - } - } - } - - private fun reloadChatRooms() { - val listViewModel = requireActivity().run { - ViewModelProvider(this)[ChatRoomsListViewModel::class.java] - } - listViewModel.updateChatRooms() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/ConferencesSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/ConferencesSettingsFragment.kt deleted file mode 100644 index 02bac24ab..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/ConferencesSettingsFragment.kt +++ /dev/null @@ -1,43 +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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModelProvider -import org.linphone.R -import org.linphone.activities.main.settings.viewmodels.ConferencesSettingsViewModel -import org.linphone.databinding.SettingsConferencesFragmentBinding - -class ConferencesSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: ConferencesSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_conferences_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - viewModel = ViewModelProvider(this)[ConferencesSettingsViewModel::class.java] - binding.viewModel = viewModel - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt deleted file mode 100644 index f949fe0d6..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/ContactsSettingsFragment.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.content.pm.PackageManager -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.main.settings.SettingListenerStub -import org.linphone.activities.main.settings.viewmodels.ContactsSettingsViewModel -import org.linphone.activities.navigateToLdapSettings -import org.linphone.core.tools.Log -import org.linphone.databinding.SettingsContactsFragmentBinding -import org.linphone.utils.PermissionHelper -import org.linphone.utils.ShortcutsHelper - -class ContactsSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: ContactsSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_contacts_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - viewModel = ViewModelProvider(this)[ContactsSettingsViewModel::class.java] - binding.viewModel = viewModel - - viewModel.launcherShortcutsEvent.observe( - viewLifecycleOwner - ) { - it.consume { newValue -> - if (newValue) { - ShortcutsHelper.createShortcutsToContacts(requireContext()) - } else { - ShortcutsHelper.removeShortcuts(requireContext()) - if (corePreferences.chatRoomShortcuts) { - ShortcutsHelper.createShortcutsToChatRooms(requireContext()) - } - } - } - } - - viewModel.askWriteContactsPermissionForPresenceStorageEvent.observe( - viewLifecycleOwner - ) { - it.consume { - Log.i( - "[Contacts Settings] Asking for WRITE_CONTACTS permission to be able to store presence" - ) - requestPermissions(arrayOf(android.Manifest.permission.WRITE_CONTACTS), 1) - } - } - - viewModel.publishPresenceToggledEvent.observe( - viewLifecycleOwner - ) { - it.consume { - sharedViewModel.publishPresenceToggled.value = true - } - } - - viewModel.ldapNewSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - Log.i("[Contacts Settings] Clicked on new LDAP config") - navigateToLdapSettings(-1) - } - } - - viewModel.ldapSettingsClickedEvent.observe( - viewLifecycleOwner - ) { - it.consume { index -> - Log.i("[Contacts Settings] Clicked on LDAP config with index: $index") - navigateToLdapSettings(index) - } - } - - if (corePreferences.enableNativeAddressBookIntegration) { - if (!PermissionHelper.required(requireContext()).hasReadContactsPermission()) { - Log.i("[Contacts Settings] Asking for READ_CONTACTS permission") - requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0) - } - } - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - when (requestCode) { - 0 -> { - val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED - if (granted) { - Log.i("[Contacts Settings] READ_CONTACTS permission granted") - viewModel.readContactsPermissionGranted.value = true - coreContext.fetchContacts() - } else { - Log.w("[Contacts Settings] READ_CONTACTS permission denied") - } - } - 1 -> { - val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED - if (granted) { - Log.i("[Contacts Settings] WRITE_CONTACTS permission granted") - corePreferences.storePresenceInNativeContact = true - } else { - Log.w("[Contacts Settings] WRITE_CONTACTS permission denied") - } - } - } - } - - override fun onResume() { - super.onResume() - viewModel.updateLdapConfigurationsList() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/GenericSettingFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/GenericSettingFragment.kt deleted file mode 100644 index 6d2844a71..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/GenericSettingFragment.kt +++ /dev/null @@ -1,33 +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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.os.Bundle -import android.view.View -import androidx.databinding.ViewDataBinding -import org.linphone.activities.GenericFragment - -abstract class GenericSettingFragment : GenericFragment() { - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - useMaterialSharedAxisXForwardAnimation = sharedViewModel.isSlidingPaneSlideable.value == false - - super.onViewCreated(view, savedInstanceState) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/LdapSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/LdapSettingsFragment.kt deleted file mode 100644 index aba760799..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/LdapSettingsFragment.kt +++ /dev/null @@ -1,66 +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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModelProvider -import org.linphone.R -import org.linphone.activities.main.settings.viewmodels.LdapSettingsViewModel -import org.linphone.activities.main.settings.viewmodels.LdapSettingsViewModelFactory -import org.linphone.core.tools.Log -import org.linphone.databinding.SettingsLdapFragmentBinding - -class LdapSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: LdapSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_ldap_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - val configIndex = arguments?.getInt("LdapConfigIndex") - if (configIndex == null) { - Log.e("[LDAP Settings] Config index not specified!") - goBack() - return - } - - try { - viewModel = ViewModelProvider(this, LdapSettingsViewModelFactory(configIndex))[LdapSettingsViewModel::class.java] - } catch (nsee: NoSuchElementException) { - Log.e("[LDAP Settings] Failed to find LDAP object, aborting!") - goBack() - return - } - binding.viewModel = viewModel - - viewModel.ldapConfigDeletedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - goBack() - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/NetworkSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/NetworkSettingsFragment.kt deleted file mode 100644 index f990f2ada..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/NetworkSettingsFragment.kt +++ /dev/null @@ -1,43 +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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModelProvider -import org.linphone.R -import org.linphone.activities.main.settings.viewmodels.NetworkSettingsViewModel -import org.linphone.databinding.SettingsNetworkFragmentBinding - -class NetworkSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: NetworkSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_network_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - viewModel = ViewModelProvider(this)[NetworkSettingsViewModel::class.java] - binding.viewModel = viewModel - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt deleted file mode 100644 index 72cc41def..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/SettingsFragment.kt +++ /dev/null @@ -1,186 +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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.os.Bundle -import android.view.View -import androidx.core.view.doOnPreDraw -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.fragment.NavHostFragment -import androidx.slidingpanelayout.widget.SlidingPaneLayout -import com.google.android.material.transition.MaterialSharedAxis -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.* -import org.linphone.activities.main.fragments.MasterFragment -import org.linphone.activities.main.fragments.SecureFragment -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.activities.main.settings.viewmodels.SettingsViewModel -import org.linphone.activities.navigateToAccountSettings -import org.linphone.activities.navigateToAudioSettings -import org.linphone.activities.navigateToTunnelSettings -import org.linphone.activities.navigateToVideoSettings -import org.linphone.core.tools.Log -import org.linphone.databinding.SettingsFragmentBinding - -class SettingsFragment : SecureFragment() { - private lateinit var viewModel: SettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - useMaterialSharedAxisXForwardAnimation = false - if (corePreferences.enableAnimations) { - enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, false) - } - - /* Shared view model & sliding pane related */ - - view.doOnPreDraw { sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable } - - // Account settings loading can take some time, so wait until it is ready before opening the pane - sharedViewModel.accountSettingsFragmentOpenedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - binding.slidingPane.openPane() - } - } - - sharedViewModel.layoutChangedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - sharedViewModel.isSlidingPaneSlideable.value = binding.slidingPane.isSlideable - if (binding.slidingPane.isSlideable) { - val navHostFragment = - childFragmentManager.findFragmentById(R.id.settings_nav_container) as NavHostFragment - if (navHostFragment.navController.currentDestination?.id == R.id.emptySettingsFragment) { - Log.i( - "[Settings] Foldable device has been folded, closing side pane with empty fragment" - ) - binding.slidingPane.closePane() - } - } - } - } - - requireActivity().onBackPressedDispatcher.addCallback( - viewLifecycleOwner, - MasterFragment.SlidingPaneBackPressedCallback(binding.slidingPane) - ) - - binding.slidingPane.lockMode = SlidingPaneLayout.LOCK_MODE_LOCKED - - /* End of shared view model & sliding pane related */ - - viewModel = ViewModelProvider(this)[SettingsViewModel::class.java] - binding.viewModel = viewModel - - sharedViewModel.accountRemoved.observe( - viewLifecycleOwner - ) { - Log.i("[Settings] Account removed, update accounts list") - viewModel.updateAccountsList() - } - - sharedViewModel.defaultAccountChanged.observe( - viewLifecycleOwner - ) { - Log.i("[Settings] Default account changed, update accounts list") - viewModel.updateAccountsList() - } - - val identity = arguments?.getString("Identity") - if (identity != null) { - Log.i("[Settings] Found identity parameter in arguments: $identity") - arguments?.clear() - navigateToAccountSettings(identity) - } - - viewModel.accountsSettingsListener = object : SettingListenerStub() { - override fun onAccountClicked(identity: String) { - Log.i("[Settings] Navigation to settings for account with identity: $identity") - navigateToAccountSettings(identity) - } - } - - viewModel.tunnelSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - navigateToTunnelSettings(binding.slidingPane) - } - } - - viewModel.audioSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - navigateToAudioSettings(binding.slidingPane) - } - } - - viewModel.videoSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - navigateToVideoSettings(binding.slidingPane) - } - } - - viewModel.callSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - navigateToCallSettings(binding.slidingPane) - } - } - - viewModel.chatSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - navigateToChatSettings(binding.slidingPane) - } - } - - viewModel.networkSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - navigateToNetworkSettings(binding.slidingPane) - } - } - - viewModel.contactsSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - navigateToContactsSettings(binding.slidingPane) - } - } - - viewModel.advancedSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - navigateToAdvancedSettings(binding.slidingPane) - } - } - - viewModel.conferencesSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - navigateToConferencesSettings(binding.slidingPane) - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/TunnelSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/TunnelSettingsFragment.kt deleted file mode 100644 index 947f7bd6f..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/TunnelSettingsFragment.kt +++ /dev/null @@ -1,43 +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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModelProvider -import org.linphone.R -import org.linphone.activities.main.settings.viewmodels.TunnelSettingsViewModel -import org.linphone.databinding.SettingsTunnelFragmentBinding - -class TunnelSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: TunnelSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_tunnel_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - viewModel = ViewModelProvider(this)[TunnelSettingsViewModel::class.java] - binding.viewModel = viewModel - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/fragments/VideoSettingsFragment.kt b/app/src/main/java/org/linphone/activities/main/settings/fragments/VideoSettingsFragment.kt deleted file mode 100644 index 6ad8561bd..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/fragments/VideoSettingsFragment.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings.fragments - -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import androidx.databinding.DataBindingUtil -import androidx.databinding.ViewDataBinding -import androidx.lifecycle.ViewModelProvider -import org.linphone.BR -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.activities.main.settings.viewmodels.VideoSettingsViewModel -import org.linphone.core.tools.Log -import org.linphone.databinding.SettingsVideoFragmentBinding -import org.linphone.utils.PermissionHelper - -class VideoSettingsFragment : GenericSettingFragment() { - private lateinit var viewModel: VideoSettingsViewModel - - override fun getLayoutId(): Int = R.layout.settings_video_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - binding.sharedMainViewModel = sharedViewModel - - viewModel = ViewModelProvider(this)[VideoSettingsViewModel::class.java] - binding.viewModel = viewModel - - initVideoCodecsList() - - if (!PermissionHelper.required(requireContext()).hasCameraPermission()) { - Log.i("[Video Settings] Asking for CAMERA permission") - requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 0) - } - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED - if (granted) { - Log.i("[Video Settings] CAMERA permission granted") - coreContext.core.reloadVideoDevices() - viewModel.initCameraDevicesList() - } else { - Log.w("[Video Settings] CAMERA permission denied") - } - } - - private fun initVideoCodecsList() { - val list = arrayListOf() - for (payload in coreContext.core.videoPayloadTypes) { - val binding = DataBindingUtil.inflate( - LayoutInflater.from(requireContext()), - R.layout.settings_widget_switch_and_text, - null, - false - ) - binding.setVariable(BR.switch_title, payload.mimeType) - binding.setVariable(BR.switch_subtitle, "") - binding.setVariable(BR.text_title, "recv-fmtp") - binding.setVariable(BR.text_subtitle, "") - binding.setVariable(BR.defaultValue, payload.recvFmtp) - binding.setVariable(BR.checked, payload.enabled()) - binding.setVariable( - BR.listener, - object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - payload.enable(newValue) - } - - override fun onTextValueChanged(newValue: String) { - payload.recvFmtp = newValue - } - } - ) - binding.lifecycleOwner = viewLifecycleOwner - list.add(binding) - } - viewModel.videoCodecs.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt deleted file mode 100644 index 1b71748ec..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AccountSettingsViewModel.kt +++ /dev/null @@ -1,561 +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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import java.lang.NumberFormatException -import kotlin.collections.ArrayList -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.Event -import org.linphone.utils.LinphoneUtils - -class AccountSettingsViewModelFactory(private val identity: String) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - for (account in coreContext.core.accountList) { - if (account.params.identityAddress?.asStringUriOnly() == identity) { - return AccountSettingsViewModel(account) as T - } - } - val defaultAccount = coreContext.core.defaultAccount - if (defaultAccount != null) { - return AccountSettingsViewModel(defaultAccount) as T - } - - val firstAccount = coreContext.core.accountList.first() - return AccountSettingsViewModel(firstAccount) as T - } -} - -class AccountSettingsViewModel(val account: Account) : GenericSettingsViewModel() { - val isDefault = MutableLiveData() - - val displayName = MutableLiveData() - - val identity = MutableLiveData() - - val iconResource = MutableLiveData() - val iconContentDescription = MutableLiveData() - - lateinit var accountsSettingsListener: SettingListenerStub - - val waitForUnregister = MutableLiveData() - - val accountRemovedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val accountDefaultEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val displayUsernameInsteadOfIdentity = corePreferences.replaceSipUriByUsername - - private var accountToDelete: Account? = null - - val listener: AccountListenerStub = object : AccountListenerStub() { - override fun onRegistrationStateChanged( - account: Account, - state: RegistrationState, - message: String - ) { - if (state == RegistrationState.Cleared && account == accountToDelete) { - Log.i( - "[Account Settings] Account to remove ([${account.params.identityAddress?.asStringUriOnly()}]) registration is now cleared, removing it" - ) - waitForUnregister.value = false - deleteAccount(account) - } else { - update() - if (state == RegistrationState.Ok) { - coreContext.contactsManager.updateLocalContacts() - } - } - } - } - - /* Settings part */ - - val userNameListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = account.params.clone() - val identity = params.identityAddress - if (identity != null) { - val newIdentityAddress = identity.clone() - newIdentityAddress.username = newValue - params.identityAddress = newIdentityAddress - account.params = params - } else { - Log.e("[Account Settings] Account doesn't have an identity yet") - - val domain = params.domain - val newIdentityAddress = Factory.instance().createAddress("sip:$newValue@$domain") - if (newIdentityAddress != null) { - params.identityAddress = newIdentityAddress - account.params = params - } else { - Log.e( - "[Account Settings] Failed to create identity address sip:$newValue@$domain" - ) - } - } - } - } - val userName = MutableLiveData() - - val userIdListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val authInfo = account.findAuthInfo() - if (authInfo != null) { - val newAuthInfo = authInfo.clone() - newAuthInfo.userid = newValue - core.removeAuthInfo(authInfo) - core.addAuthInfo(newAuthInfo) - } else { - Log.e("[Account Settings] Failed to find the matching auth info") - } - } - } - val userId = MutableLiveData() - - val passwordListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val authInfo = account.findAuthInfo() - if (authInfo != null) { - val newAuthInfo = authInfo.clone() - newAuthInfo.password = newValue - core.removeAuthInfo(authInfo) - core.addAuthInfo(newAuthInfo) - } else { - Log.w("[Account Settings] Failed to find the matching auth info") - val params = account.params - val identity = params.identityAddress - if (identity != null && identity.username != null) { - val newAuthInfo = Factory.instance() - .createAuthInfo( - identity.username!!, - userId.value, - newValue, - null, - null, - identity.domain - ) - core.addAuthInfo(newAuthInfo) - } else { - Log.e( - "[Account Settings] Failed to find the user's identity, can't create a new auth info" - ) - } - } - } - } - val password = MutableLiveData() - - val domainListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = account.params.clone() - val identity = params.identityAddress - if (identity != null) { - val authInfo = account.findAuthInfo() - if (authInfo != null) { - val newAuthInfo = authInfo.clone() - newAuthInfo.domain = newValue - core.removeAuthInfo(authInfo) - core.addAuthInfo(newAuthInfo) - } else { - Log.e("[Account Settings] Failed to find the matching auth info") - } - - identity.domain = newValue - params.identityAddress = identity - account.params = params - } else { - Log.e("[Account Settings] Account doesn't have an identity yet") - } - } - } - val domain = MutableLiveData() - - val displayNameListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = account.params.clone() - val identity = params.identityAddress - if (identity != null) { - identity.displayName = newValue - params.identityAddress = identity - account.params = params - } else { - Log.e("[Account Settings] Account doesn't have an identity yet") - } - } - } - // displayName mutable is above - - val disableListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = account.params.clone() - params.isRegisterEnabled = !newValue - account.params = params - } - } - val disable = MutableLiveData() - - val isDefaultListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - if (newValue) { - core.defaultAccount = account - accountDefaultEvent.value = Event(true) - } - } - } - // isDefault mutable is above - - private fun deleteAccount(account: Account) { - val authInfo = account.findAuthInfo() - if (authInfo != null) { - Log.i("[Account Settings] Found auth info $authInfo, removing it.") - core.removeAuthInfo(authInfo) - } else { - Log.w("[Account Settings] Couldn't find matching auth info...") - } - - core.removeAccount(account) - accountToDelete = null - accountRemovedEvent.value = Event(true) - } - - val deleteAccountRequiredEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - val deleteListener = object : SettingListenerStub() { - override fun onClicked() { - deleteAccountRequiredEvent.value = Event(true) - } - } - - val pushNotificationListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = account.params.clone() - params.pushNotificationAllowed = newValue - account.params = params - } - } - val pushNotification = MutableLiveData() - val pushNotificationsAvailable = MutableLiveData() - - val transportListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - val params = account.params.clone() - params.transport = TransportType.fromInt(position) - account.params = params - proxy.value = account.params.serverAddress?.asStringUriOnly() - } - } - val transportIndex = MutableLiveData() - val transportLabels = MutableLiveData>() - - val proxyListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = account.params.clone() - val address = Factory.instance().createAddress(newValue) - if (address != null) { - params.serverAddress = address - account.params = params - transportIndex.value = account.params.transport.toInt() - } else { - Log.e("[Account Settings] Couldn't parse address: $newValue") - } - } - } - val proxy = MutableLiveData() - - val outboundProxyListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = account.params.clone() - params.isOutboundProxyEnabled = newValue - account.params = params - } - } - val outboundProxy = MutableLiveData() - - val stunServerListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = account.params.clone() - val natPolicy = params.natPolicy - val newNatPolicy = natPolicy?.clone() ?: core.createNatPolicy() - newNatPolicy.stunServer = newValue - newNatPolicy.isStunEnabled = newValue.isNotEmpty() - params.natPolicy = newNatPolicy - account.params = params - stunServer.value = newValue - } - } - val stunServer = MutableLiveData() - - val iceListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = account.params.clone() - val natPolicy = params.natPolicy - val newNatPolicy = natPolicy?.clone() ?: core.createNatPolicy() - newNatPolicy.isIceEnabled = newValue - params.natPolicy = newNatPolicy - account.params = params - } - } - val ice = MutableLiveData() - - val avpfListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = account.params.clone() - params.avpfMode = if (newValue) AVPFMode.Enabled else AVPFMode.Disabled - account.params = params - } - } - val avpf = MutableLiveData() - - val avpfRrIntervalListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val params = account.params.clone() - params.avpfRrInterval = newValue.toInt() - account.params = params - } catch (nfe: NumberFormatException) { - Log.e("[Account Settings] Failed to set AVPF RR interval ($newValue): $nfe") - } - } - } - val avpfRrInterval = MutableLiveData() - - val expiresListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val params = account.params.clone() - params.expires = newValue.toInt() - account.params = params - } catch (nfe: NumberFormatException) { - Log.e("[Account Settings] Failed to set expires ($newValue): $nfe") - } - } - } - val expires = MutableLiveData() - - val prefixListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = account.params.clone() - params.internationalPrefix = newValue - account.params = params - } - } - val prefix = MutableLiveData() - - val dialPrefixListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = account.params.clone() - params.useInternationalPrefixForCallsAndChats = newValue - account.params = params - } - } - val dialPrefix = MutableLiveData() - - val escapePlusListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = account.params.clone() - params.isDialEscapePlusEnabled = newValue - account.params = params - } - } - val escapePlus = MutableLiveData() - - val linkPhoneNumberListener = object : SettingListenerStub() { - override fun onClicked() { - linkPhoneNumberEvent.value = Event(true) - } - } - val linkPhoneNumberEvent = MutableLiveData>() - val hideLinkPhoneNumber = MutableLiveData() - - val conferenceFactoryUriListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = account.params.clone() - Log.i( - "[Account Settings] Forcing conference factory on account ${params.identityAddress?.asString()} to value: $newValue" - ) - params.conferenceFactoryUri = newValue - account.params = params - } - } - val conferenceFactoryUri = MutableLiveData() - - val audioVideoConferenceFactoryUriListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = account.params.clone() - val uri = coreContext.core.interpretUrl(newValue, false) - Log.i( - "[Account Settings] Forcing audio/video conference factory on account ${params.identityAddress?.asString()} to value: $newValue" - ) - params.audioVideoConferenceFactoryAddress = uri - account.params = params - } - } - val audioVideoConferenceFactoryUri = MutableLiveData() - - val limeServerUrlListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = account.params.clone() - params.limeServerUrl = newValue - account.params = params - } - } - val limeServerUrl = MutableLiveData() - - val disableBundleModeListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = account.params.clone() - params.isRtpBundleEnabled = !newValue - account.params = params - } - } - val disableBundleMode = MutableLiveData() - - init { - update() - account.addListener(listener) - initTransportList() - } - - override fun onCleared() { - destroy() - super.onCleared() - } - - fun destroy() { - accountsSettingsListener = object : SettingListenerStub() {} - account.removeListener(listener) - } - - private fun update() { - isDefault.value = core.defaultAccount == account - val params = account.params - val identityAddress = params.identityAddress - if (identityAddress != null) { - displayName.value = identityAddress.displayName ?: "" - identity.value = identityAddress.asStringUriOnly() - } - - iconResource.value = when (account.state) { - RegistrationState.Ok -> R.drawable.led_registered - RegistrationState.Failed -> R.drawable.led_error - RegistrationState.Progress, RegistrationState.Refreshing -> R.drawable.led_registration_in_progress - else -> R.drawable.led_not_registered - } - iconContentDescription.value = when (account.state) { - RegistrationState.Ok -> R.string.status_connected - RegistrationState.Progress, RegistrationState.Refreshing -> R.string.status_in_progress - RegistrationState.Failed -> R.string.status_error - else -> R.string.status_not_connected - } - - userName.value = params.identityAddress?.username - userId.value = account.findAuthInfo()?.userid - domain.value = params.identityAddress?.domain - disable.value = !params.isRegisterEnabled - pushNotification.value = params.pushNotificationAllowed - pushNotificationsAvailable.value = LinphoneUtils.isPushNotificationAvailable() - proxy.value = params.serverAddress?.asStringUriOnly() - outboundProxy.value = params.isOutboundProxyEnabled - stunServer.value = params.natPolicy?.stunServer - ice.value = params.natPolicy?.isIceEnabled - avpf.value = params.avpfMode == AVPFMode.Enabled - avpfRrInterval.value = params.avpfRrInterval - expires.value = params.expires - prefix.value = params.internationalPrefix - dialPrefix.value = params.useInternationalPrefixForCallsAndChats - escapePlus.value = params.isDialEscapePlusEnabled - - conferenceFactoryUri.value = params.conferenceFactoryUri - audioVideoConferenceFactoryUri.value = params.audioVideoConferenceFactoryAddress?.asStringUriOnly() - limeServerUrl.value = params.limeServerUrl - - hideLinkPhoneNumber.value = corePreferences.hideLinkPhoneNumber || params.identityAddress?.domain != corePreferences.defaultDomain - disableBundleMode.value = !params.isRtpBundleEnabled - } - - private fun initTransportList() { - val labels = arrayListOf() - - // Keep following in the same order as TransportType enum - labels.add(prefs.getString(R.string.account_settings_transport_udp)) - labels.add(prefs.getString(R.string.account_settings_transport_tcp)) - labels.add(prefs.getString(R.string.account_settings_transport_tls)) - if (corePreferences.allowDtlsTransport) { - labels.add(prefs.getString(R.string.account_settings_transport_dtls)) - } - - transportLabels.value = labels - transportIndex.value = account.params.transport.toInt() - } - - fun startDeleteAccount() { - Log.i( - "[Account Settings] Starting to delete account [${account.params.identityAddress?.asStringUriOnly()}]" - ) - accountToDelete = account - - val registered = account.state == RegistrationState.Ok - waitForUnregister.value = registered - - if (core.defaultAccount == account) { - Log.i("[Account Settings] Account was default, let's look for a replacement") - for (accountIterator in core.accountList) { - if (account != accountIterator) { - core.defaultAccount = accountIterator - Log.i( - "[Account Settings] New default account is [${accountIterator.params.identityAddress?.asStringUriOnly()}]" - ) - break - } - } - } - - val params = account.params.clone() - params.isRegisterEnabled = false - account.params = params - - if (!registered) { - Log.w( - "[Account Settings] Account isn't registered, don't unregister before removing it" - ) - deleteAccount(account) - } else { - Log.i( - "[Account Settings] Waiting for account registration to be cleared before removing it" - ) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt deleted file mode 100644 index 118666070..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AdvancedSettingsViewModel.kt +++ /dev/null @@ -1,187 +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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.MutableLiveData -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.activities.main.viewmodels.LogsUploadViewModel -import org.linphone.core.CoreContext -import org.linphone.core.Factory -import org.linphone.core.LogLevel -import org.linphone.utils.Event - -class AdvancedSettingsViewModel : LogsUploadViewModel() { - private val prefs = corePreferences - private val core = coreContext.core - - val debugModeListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.debugLogs = newValue - val logLevel = if (newValue) LogLevel.Message else LogLevel.Error - Factory.instance().loggingService.setLogLevel(logLevel) - } - } - val debugMode = MutableLiveData() - - val logsServerUrlListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - core.logCollectionUploadServerUrl = newValue - } - } - val logsServerUrl = MutableLiveData() - - val sendDebugLogsListener = object : SettingListenerStub() { - override fun onClicked() { - uploadLogs() - } - } - - val resetDebugLogsListener = object : SettingListenerStub() { - override fun onClicked() { - resetLogs() - } - } - - val backgroundModeListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.keepServiceAlive = newValue - - if (newValue) { - coreContext.notificationsManager.startForeground() - } else { - coreContext.notificationsManager.stopForegroundNotificationIfPossible() - } - } - } - val backgroundMode = MutableLiveData() - val backgroundModeEnabled = MutableLiveData() - - val autoStartListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.autoStart = newValue - } - } - val autoStart = MutableLiveData() - - val darkModeListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - darkModeIndex.value = position - val value = darkModeValues[position] - prefs.darkMode = value - setNightModeEvent.value = Event(value) - } - } - val darkModeIndex = MutableLiveData() - val darkModeLabels = MutableLiveData>() - private val darkModeValues = arrayListOf(-1, 0, 1) - val setNightModeEvent = MutableLiveData>() - - val animationsListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.enableAnimations = newValue - } - } - val animations = MutableLiveData() - - val deviceNameListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - prefs.deviceName = newValue - } - } - val deviceName = MutableLiveData() - - val remoteProvisioningUrlListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - if (newValue.isEmpty()) { - core.provisioningUri = null - } else { - core.provisioningUri = newValue - } - } - } - val remoteProvisioningUrl = MutableLiveData() - - val vfsListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.vfsEnabled = newValue - if (newValue) { - CoreContext.activateVFS() - // Don't do that when VFS is enabled - prefs.makePublicMediaFilesDownloaded = false - } - } - } - val vfs = MutableLiveData() - - val disableSecureFragmentListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.disableSecureMode = newValue - } - } - val disableSecureFragment = MutableLiveData() - - val goToBatterySettingsListener = object : SettingListenerStub() { - override fun onClicked() { - goToBatterySettingsEvent.value = Event(true) - } - } - val goToBatterySettingsEvent = MutableLiveData>() - val batterySettingsVisibility = MutableLiveData() - - val goToPowerManagerSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - goToPowerManagerSettingsEvent.value = Event(true) - } - } - val goToPowerManagerSettingsEvent = MutableLiveData>() - val powerManagerSettingsVisibility = MutableLiveData() - - val goToAndroidSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - goToAndroidSettingsEvent.value = Event(true) - } - } - val goToAndroidSettingsEvent = MutableLiveData>() - - init { - debugMode.value = prefs.debugLogs - logsServerUrl.value = core.logCollectionUploadServerUrl - backgroundMode.value = prefs.keepServiceAlive - autoStart.value = prefs.autoStart - - val labels = arrayListOf() - labels.add(prefs.getString(R.string.advanced_settings_dark_mode_label_auto)) - labels.add(prefs.getString(R.string.advanced_settings_dark_mode_label_no)) - labels.add(prefs.getString(R.string.advanced_settings_dark_mode_label_yes)) - darkModeLabels.value = labels - darkModeIndex.value = darkModeValues.indexOf(prefs.darkMode) - - animations.value = prefs.enableAnimations - deviceName.value = prefs.deviceName - remoteProvisioningUrl.value = core.provisioningUri - vfs.value = prefs.vfsEnabled - disableSecureFragment.value = prefs.disableSecureMode - - batterySettingsVisibility.value = true - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt deleted file mode 100644 index 9e7bc11ce..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/AudioSettingsViewModel.kt +++ /dev/null @@ -1,296 +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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.databinding.ViewDataBinding -import androidx.lifecycle.MutableLiveData -import java.lang.NumberFormatException -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.core.AudioDevice -import org.linphone.core.Core -import org.linphone.core.CoreListenerStub -import org.linphone.core.EcCalibratorStatus -import org.linphone.utils.Event -import org.linphone.utils.PermissionHelper - -class AudioSettingsViewModel : GenericSettingsViewModel() { - val askAudioRecordPermissionForEchoCancellerCalibrationEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - val askAudioRecordPermissionForEchoTesterEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val softwareEchoCancellerListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isEchoCancellationEnabled = newValue - if (!newValue) { - core.resetEchoCancellationCalibration() - softwareEchoCalibration.value = prefs.getString( - R.string.audio_settings_echo_canceller_calibration_summary - ) - } - } - } - val softwareEchoCanceller = MutableLiveData() - val listener = object : CoreListenerStub() { - override fun onEcCalibrationResult(core: Core, status: EcCalibratorStatus, delayMs: Int) { - if (status == EcCalibratorStatus.InProgress) return - echoCancellerCalibrationFinished(status, delayMs) - } - } - - val softwareEchoCancellerCalibrationListener = object : SettingListenerStub() { - override fun onClicked() { - if (PermissionHelper.get().hasRecordAudioPermission()) { - startEchoCancellerCalibration() - } else { - askAudioRecordPermissionForEchoCancellerCalibrationEvent.value = Event(true) - } - } - } - val softwareEchoCalibration = MutableLiveData() - - val echoTesterListener = object : SettingListenerStub() { - override fun onClicked() { - if (PermissionHelper.get().hasRecordAudioPermission()) { - if (echoTesterIsRunning) { - stopEchoTester() - } else { - startEchoTester() - } - } else { - askAudioRecordPermissionForEchoTesterEvent.value = Event(true) - } - } - } - private var echoTesterIsRunning = false - val echoTesterStatus = MutableLiveData() - val showEchoTester = MutableLiveData() - - val adaptiveRateControlListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isAdaptiveRateControlEnabled = newValue - } - } - val adaptiveRateControl = MutableLiveData() - - val inputAudioDeviceListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - val values = inputAudioDeviceValues.value.orEmpty() - if (values.size > position) { - core.defaultInputAudioDevice = values[position] - } - } - } - val inputAudioDeviceIndex = MutableLiveData() - val inputAudioDeviceLabels = MutableLiveData>() - private val inputAudioDeviceValues = MutableLiveData>() - - val outputAudioDeviceListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - val values = outputAudioDeviceValues.value.orEmpty() - if (values.size > position) { - core.defaultOutputAudioDevice = values[position] - } - } - } - val outputAudioDeviceIndex = MutableLiveData() - val outputAudioDeviceLabels = MutableLiveData>() - private val outputAudioDeviceValues = MutableLiveData>() - - val preferBluetoothDevicesListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.routeAudioToBluetoothIfAvailable = newValue - } - } - val preferBluetoothDevices = MutableLiveData() - - val codecBitrateListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - for (payloadType in core.audioPayloadTypes) { - if (payloadType.isVbr) { - payloadType.normalBitrate = codecBitrateValues[position] - } - } - } - } - val codecBitrateIndex = MutableLiveData() - val codecBitrateLabels = MutableLiveData>() - private val codecBitrateValues = arrayListOf(10, 15, 20, 36, 64, 128) - - val microphoneGainListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - core.micGainDb = newValue.toFloat() - } catch (_: NumberFormatException) { - } - } - } - val microphoneGain = MutableLiveData() - - val playbackGainListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - core.playbackGainDb = newValue.toFloat() - } catch (_: NumberFormatException) { - } - } - } - val playbackGain = MutableLiveData() - - val audioCodecs = MutableLiveData>() - - init { - softwareEchoCanceller.value = core.isEchoCancellationEnabled - adaptiveRateControl.value = core.isAdaptiveRateControlEnabled - softwareEchoCalibration.value = if (core.echoCancellationCalibration > 0) { - prefs.getString(R.string.audio_settings_echo_cancellation_calibration_value).format( - core.echoCancellationCalibration - ) - } else { - prefs.getString(R.string.audio_settings_echo_canceller_calibration_summary) - } - echoTesterStatus.value = prefs.getString(R.string.audio_settings_echo_tester_summary) - showEchoTester.value = prefs.debugLogs // Don't show echo tester unless debug mode is enabled, may confuse user over what it should be used for - - preferBluetoothDevices.value = prefs.routeAudioToBluetoothIfAvailable - initInputAudioDevicesList() - initOutputAudioDevicesList() - initCodecBitrateList() - microphoneGain.value = core.micGainDb - playbackGain.value = core.playbackGainDb - } - - fun startEchoCancellerCalibration() { - if (echoTesterIsRunning) { - stopEchoTester() - } - - core.addListener(listener) - core.startEchoCancellerCalibration() - softwareEchoCalibration.value = prefs.getString( - R.string.audio_settings_echo_cancellation_calibration_started - ) - } - - fun echoCancellerCalibrationFinished(status: EcCalibratorStatus, delay: Int) { - core.removeListener(listener) - - when (status) { - EcCalibratorStatus.DoneNoEcho -> { - softwareEchoCalibration.value = prefs.getString( - R.string.audio_settings_echo_cancellation_calibration_no_echo - ) - softwareEchoCanceller.value = false - } - EcCalibratorStatus.Done -> { - softwareEchoCalibration.value = prefs.getString( - R.string.audio_settings_echo_cancellation_calibration_value - ).format(delay) - softwareEchoCanceller.value = true - } - EcCalibratorStatus.Failed -> { - softwareEchoCalibration.value = prefs.getString( - R.string.audio_settings_echo_cancellation_calibration_failed - ) - } - EcCalibratorStatus.InProgress -> { // We should never get here but still - softwareEchoCalibration.value = prefs.getString( - R.string.audio_settings_echo_cancellation_calibration_started - ) - } - } - } - - fun startEchoTester() { - echoTesterIsRunning = true - echoTesterStatus.value = prefs.getString( - R.string.audio_settings_echo_tester_summary_is_running - ) - core.startEchoTester(0) - } - - fun stopEchoTester() { - echoTesterIsRunning = false - echoTesterStatus.value = prefs.getString( - R.string.audio_settings_echo_tester_summary_is_stopped - ) - core.stopEchoTester() - } - - private fun initInputAudioDevicesList() { - val labels = arrayListOf() - val values = arrayListOf() - var index = 0 - val default = core.defaultInputAudioDevice - - for (audioDevice in core.extendedAudioDevices) { - if (audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityRecord)) { - labels.add(audioDevice.id) - values.add(audioDevice) - if (audioDevice.id == default?.id) { - inputAudioDeviceIndex.value = index - } - index += 1 - } - } - inputAudioDeviceLabels.value = labels - inputAudioDeviceValues.value = values - } - - private fun initOutputAudioDevicesList() { - val labels = arrayListOf() - val values = arrayListOf() - var index = 0 - val default = core.defaultOutputAudioDevice - - for (audioDevice in core.extendedAudioDevices) { - if (audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) { - labels.add(audioDevice.id) - values.add(audioDevice) - if (audioDevice.id == default?.id) { - outputAudioDeviceIndex.value = index - } - index += 1 - } - } - outputAudioDeviceLabels.value = labels - outputAudioDeviceValues.value = values - } - - private fun initCodecBitrateList() { - val labels = arrayListOf() - for (value in codecBitrateValues) { - labels.add("$value kbits/s") - } - codecBitrateLabels.value = labels - - var currentValue = 36 - for (payloadType in core.audioPayloadTypes) { - if (payloadType.isVbr && payloadType.normalBitrate in codecBitrateValues) { - currentValue = payloadType.normalBitrate - break - } - } - codecBitrateIndex.value = codecBitrateValues.indexOf(currentValue) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt deleted file mode 100644 index 45fb318f1..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/CallSettingsViewModel.kt +++ /dev/null @@ -1,317 +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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import android.os.Vibrator -import androidx.lifecycle.MutableLiveData -import java.io.File -import java.util.* -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.core.MediaEncryption -import org.linphone.core.tools.Log -import org.linphone.mediastream.Version -import org.linphone.telecom.TelecomHelper -import org.linphone.utils.AppUtils -import org.linphone.utils.Event - -class CallSettingsViewModel : GenericSettingsViewModel() { - val deviceRingtoneListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.ring = if (newValue) null else prefs.defaultRingtonePath - } - } - val deviceRingtone = MutableLiveData() - - val ringtoneListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - if (position == 0) { - core.ring = null - } else { - core.ring = ringtoneValues[position] - } - } - } - val ringtoneIndex = MutableLiveData() - val ringtoneLabels = MutableLiveData>() - private val ringtoneValues = arrayListOf() - val showRingtonesList = MutableLiveData() - - val vibrateOnIncomingCallListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isVibrationOnIncomingCallEnabled = newValue - } - } - val vibrateOnIncomingCall = MutableLiveData() - val canVibrate = MutableLiveData() - - val encryptionListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - core.mediaEncryption = MediaEncryption.fromInt(encryptionValues[position]) - encryptionIndex.value = position - if (position == 0) { - encryptionMandatory.value = false - } - } - } - val encryptionIndex = MutableLiveData() - val encryptionLabels = MutableLiveData>() - private val encryptionValues = arrayListOf() - - val encryptionMandatoryListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isMediaEncryptionMandatory = newValue - } - } - val encryptionMandatory = MutableLiveData() - - val useTelecomManagerListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - if (newValue) { - enableTelecomManagerEvent.value = Event(true) - } else { - if (TelecomHelper.exists()) { - Log.i("[Call Settings] Removing Telecom Manager account & destroying singleton") - TelecomHelper.get().removeAccount() - TelecomHelper.get().destroy() - TelecomHelper.destroy() - - Log.w("[Call Settings] Disabling Telecom Manager auto-enable") - prefs.manuallyDisabledTelecomManager = true - } - prefs.useTelecomManager = false - } - } - } - val useTelecomManager = MutableLiveData() - val enableTelecomManagerEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - val api29OrHigher = MutableLiveData() - - val overlayListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.showCallOverlay = newValue - } - } - val overlay = MutableLiveData() - - val systemWideOverlayListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - if (newValue) systemWideOverlayEnabledEvent.value = Event(true) - prefs.systemWideCallOverlay = newValue - } - } - val systemWideOverlay = MutableLiveData() - val systemWideOverlayEnabledEvent = MutableLiveData>() - - val sipInfoDtmfListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.useInfoForDtmf = newValue - } - } - val sipInfoDtmf = MutableLiveData() - - val rfc2833DtmfListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.useRfc2833ForDtmf = newValue - } - } - val rfc2833Dtmf = MutableLiveData() - - val autoStartCallRecordingListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.automaticallyStartCallRecording = newValue - } - } - val autoStartCallRecording = MutableLiveData() - - val remoteCallRecordingListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isRecordAwareEnabled = newValue - } - } - val remoteCallRecording = MutableLiveData() - - val autoStartListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.callRightAway = newValue - } - } - val autoStart = MutableLiveData() - - val autoAnswerListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.autoAnswerEnabled = newValue - } - } - val autoAnswer = MutableLiveData() - - val autoAnswerDelayListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - prefs.autoAnswerDelay = newValue.toInt() - } catch (_: NumberFormatException) { - } - } - } - val autoAnswerDelay = MutableLiveData() - - val incomingTimeoutListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - core.incTimeout = newValue.toInt() - } catch (_: NumberFormatException) { - } - } - } - val incomingTimeout = MutableLiveData() - - val voiceMailUriListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - voiceMailUri.value = newValue - prefs.voiceMailUri = newValue - } - } - val voiceMailUri = MutableLiveData() - - val redirectToVoiceMailIncomingDeclinedCallsListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.redirectDeclinedCallToVoiceMail = newValue - } - } - val redirectToVoiceMailIncomingDeclinedCalls = MutableLiveData() - - val acceptEarlyMediaListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.acceptEarlyMedia = newValue - } - } - val acceptEarlyMedia = MutableLiveData() - - val ringDuringEarlyMediaListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.ringDuringIncomingEarlyMedia = newValue - } - } - val ringDuringEarlyMedia = MutableLiveData() - - val pauseCallsWhenAudioFocusIsLostListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.pauseCallsWhenAudioFocusIsLost = newValue - } - } - - val pauseCallsWhenAudioFocusIsLost = MutableLiveData() - - val goToAndroidNotificationSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - goToAndroidNotificationSettingsEvent.value = Event(true) - } - } - val goToAndroidNotificationSettingsEvent = MutableLiveData>() - - init { - initRingtonesList() - deviceRingtone.value = core.ring == null - showRingtonesList.value = prefs.showAllRingtones - - vibrateOnIncomingCall.value = core.isVibrationOnIncomingCallEnabled - val vibrator = coreContext.context.getSystemService(Vibrator::class.java) - canVibrate.value = vibrator.hasVibrator() - if (canVibrate.value == false) { - Log.w("[Call Settings] Device doesn't seem to have a vibrator, hiding related setting") - } - - initEncryptionList() - encryptionMandatory.value = core.isMediaEncryptionMandatory - - useTelecomManager.value = prefs.useTelecomManager - api29OrHigher.value = Version.sdkAboveOrEqual(Version.API29_ANDROID_10) - - overlay.value = prefs.showCallOverlay - systemWideOverlay.value = prefs.systemWideCallOverlay - sipInfoDtmf.value = core.useInfoForDtmf - rfc2833Dtmf.value = core.useRfc2833ForDtmf - autoStartCallRecording.value = prefs.automaticallyStartCallRecording - remoteCallRecording.value = core.isRecordAwareEnabled - autoStart.value = prefs.callRightAway - autoAnswer.value = prefs.autoAnswerEnabled - autoAnswerDelay.value = prefs.autoAnswerDelay - incomingTimeout.value = core.incTimeout - voiceMailUri.value = prefs.voiceMailUri - redirectToVoiceMailIncomingDeclinedCalls.value = prefs.redirectDeclinedCallToVoiceMail - acceptEarlyMedia.value = prefs.acceptEarlyMedia - ringDuringEarlyMedia.value = core.ringDuringIncomingEarlyMedia - pauseCallsWhenAudioFocusIsLost.value = prefs.pauseCallsWhenAudioFocusIsLost - } - - private fun initRingtonesList() { - val labels = arrayListOf() - labels.add(AppUtils.getString(R.string.call_settings_device_ringtone_title)) - ringtoneValues.add("") - - val directory = File(prefs.ringtonesPath) - val files = directory.listFiles() - for (ringtone in files.orEmpty()) { - if (ringtone.absolutePath.endsWith(".mkv")) { - val name = ringtone.name - .substringBefore(".") - .replace("_", " ") - .capitalize(Locale.getDefault()) - labels.add(name) - ringtoneValues.add(ringtone.absolutePath) - } - } - - ringtoneLabels.value = labels - ringtoneIndex.value = if (core.ring == null) 0 else ringtoneValues.indexOf(core.ring) - } - - private fun initEncryptionList() { - val labels = arrayListOf() - - labels.add(prefs.getString(R.string.call_settings_media_encryption_none)) - encryptionValues.add(MediaEncryption.None.toInt()) - - if (core.mediaEncryptionSupported(MediaEncryption.SRTP)) { - labels.add(prefs.getString(R.string.call_settings_media_encryption_srtp)) - encryptionValues.add(MediaEncryption.SRTP.toInt()) - } - if (core.mediaEncryptionSupported(MediaEncryption.ZRTP)) { - if (core.postQuantumAvailable) { - labels.add( - prefs.getString(R.string.call_settings_media_encryption_zrtp_post_quantum) - ) - } else { - labels.add(prefs.getString(R.string.call_settings_media_encryption_zrtp)) - } - encryptionValues.add(MediaEncryption.ZRTP.toInt()) - } - if (core.mediaEncryptionSupported(MediaEncryption.DTLS)) { - labels.add(prefs.getString(R.string.call_settings_media_encryption_dtls)) - encryptionValues.add(MediaEncryption.DTLS.toInt()) - } - - encryptionLabels.value = labels - encryptionIndex.value = encryptionValues.indexOf(core.mediaEncryption.toInt()) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt deleted file mode 100644 index d926b7b45..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ChatSettingsViewModel.kt +++ /dev/null @@ -1,173 +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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.MutableLiveData -import java.lang.NumberFormatException -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.utils.Event - -class ChatSettingsViewModel : GenericSettingsViewModel() { - val markAsReadNotifDismissalListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.markAsReadUponChatMessageNotificationDismissal = newValue - } - } - val markAsReadNotifDismissal = MutableLiveData() - - val fileSharingUrlListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - core.fileTransferServer = newValue - } - } - val fileSharingUrl = MutableLiveData() - - val autoDownloadListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - val maxSize = when (position) { - 0 -> -1 - 1 -> 0 - else -> 10000000 - } - core.maxSizeForAutoDownloadIncomingFiles = maxSize - autoDownloadMaxSize.value = maxSize - updateAutoDownloadIndexFromMaxSize(maxSize) - } - } - val autoDownloadIndex = MutableLiveData() - val autoDownloadLabels = MutableLiveData>() - - val autoDownloadMaxSizeListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val maxSize = newValue.toInt() - core.maxSizeForAutoDownloadIncomingFiles = maxSize - updateAutoDownloadIndexFromMaxSize(maxSize) - } catch (_: NumberFormatException) { - } - } - } - val autoDownloadMaxSize = MutableLiveData() - - val autoDownloadVoiceRecordingsListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isAutoDownloadVoiceRecordingsEnabled = newValue - autoDownloadVoiceRecordings.value = newValue - } - } - val autoDownloadVoiceRecordings = MutableLiveData() - - val downloadedMediaPublicListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.makePublicMediaFilesDownloaded = newValue - downloadedMediaPublic.value = newValue - } - } - val downloadedMediaPublic = MutableLiveData() - - val hideNotificationContentListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.hideChatMessageContentInNotification = newValue - hideNotificationContent.value = newValue - } - } - val hideNotificationContent = MutableLiveData() - - val useInAppFileViewerListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.useInAppFileViewerForNonEncryptedFiles = newValue - useInAppFileViewer.value = newValue - } - } - val useInAppFileViewer = MutableLiveData() - - val launcherShortcutsListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.chatRoomShortcuts = newValue - launcherShortcutsEvent.value = Event(newValue) - } - } - val launcherShortcuts = MutableLiveData() - val launcherShortcutsEvent = MutableLiveData>() - - val hideEmptyRoomsListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.hideEmptyRooms = newValue - reloadChatRoomsEvent.value = Event(true) - } - } - val hideEmptyRooms = MutableLiveData() - - val hideRoomsRemovedProxiesListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.hideRoomsFromRemovedProxies = newValue - reloadChatRoomsEvent.value = Event(true) - } - } - val hideRoomsRemovedProxies = MutableLiveData() - - val goToAndroidNotificationSettingsListener = object : SettingListenerStub() { - override fun onClicked() { - goToAndroidNotificationSettingsEvent.value = Event(true) - } - } - val goToAndroidNotificationSettingsEvent = MutableLiveData>() - - val vfs = MutableLiveData() - - val reloadChatRoomsEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - init { - markAsReadNotifDismissal.value = prefs.markAsReadUponChatMessageNotificationDismissal - downloadedMediaPublic.value = prefs.makePublicMediaFilesDownloaded && !prefs.vfsEnabled - useInAppFileViewer.value = prefs.useInAppFileViewerForNonEncryptedFiles || prefs.vfsEnabled - hideNotificationContent.value = prefs.hideChatMessageContentInNotification - initAutoDownloadList() - autoDownloadVoiceRecordings.value = core.isAutoDownloadVoiceRecordingsEnabled - launcherShortcuts.value = prefs.chatRoomShortcuts - hideEmptyRooms.value = prefs.hideEmptyRooms - hideRoomsRemovedProxies.value = prefs.hideRoomsFromRemovedProxies - fileSharingUrl.value = core.fileTransferServer - vfs.value = prefs.vfsEnabled - } - - private fun initAutoDownloadList() { - val labels = arrayListOf() - labels.add(prefs.getString(R.string.chat_settings_auto_download_never)) - labels.add(prefs.getString(R.string.chat_settings_auto_download_always)) - labels.add(prefs.getString(R.string.chat_settings_auto_download_under_size)) - autoDownloadLabels.value = labels - - val currentValue = core.maxSizeForAutoDownloadIncomingFiles - autoDownloadMaxSize.value = currentValue - updateAutoDownloadIndexFromMaxSize(currentValue) - } - - private fun updateAutoDownloadIndexFromMaxSize(maxSize: Int) { - autoDownloadIndex.value = when (maxSize) { - -1 -> 0 - 0 -> 1 - else -> 2 - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt deleted file mode 100644 index 0fadbee7f..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ConferencesSettingsViewModel.kt +++ /dev/null @@ -1,62 +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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.MutableLiveData -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.core.Conference.Layout - -class ConferencesSettingsViewModel : GenericSettingsViewModel() { - val layoutListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - core.defaultConferenceLayout = Layout.fromInt(layoutValues[position]) - layoutIndex.value = position - } - } - val layoutIndex = MutableLiveData() - val layoutLabels = MutableLiveData>() - private val layoutValues = arrayListOf() - - val enableBroadcastListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.disableBroadcastConference = !newValue - } - } - val enableBroadcast = MutableLiveData() - - init { - initLayoutsList() - enableBroadcast.value = !prefs.disableBroadcastConference - } - - private fun initLayoutsList() { - val labels = arrayListOf() - - labels.add(prefs.getString(R.string.conference_display_mode_active_speaker)) - layoutValues.add(Layout.ActiveSpeaker.toInt()) - - labels.add(prefs.getString(R.string.conference_display_mode_mosaic)) - layoutValues.add(Layout.Grid.toInt()) - - layoutLabels.value = labels - layoutIndex.value = layoutValues.indexOf(core.defaultConferenceLayout.toInt()) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt deleted file mode 100644 index 68847e3fb..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/ContactsSettingsViewModel.kt +++ /dev/null @@ -1,151 +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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.MutableLiveData -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.core.ConsolidatedPresence -import org.linphone.core.tools.Log -import org.linphone.utils.Event -import org.linphone.utils.PermissionHelper - -class ContactsSettingsViewModel : GenericSettingsViewModel() { - val askWriteContactsPermissionForPresenceStorageEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val readContactsPermissionGranted = MutableLiveData() - - val friendListSubscribeListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isFriendListSubscriptionEnabled = newValue - } - } - val friendListSubscribe = MutableLiveData() - val rlsAddressAvailable = MutableLiveData() - - val publishPresenceListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.publishPresence = newValue - - if (newValue) { - // Publish online presence when enabling setting - Log.i( - "[Contacts Settings] Presence has been enabled, PUBLISHING presence as Online" - ) - core.consolidatedPresence = ConsolidatedPresence.Online - } else { - // Unpublish presence when disabling setting - Log.i("[Contacts Settings] Presence has been disabled, un-PUBLISHING presence info") - core.consolidatedPresence = ConsolidatedPresence.Offline - } - - publishPresenceToggledEvent.value = Event(true) - } - } - val publishPresence = MutableLiveData() - val publishPresenceToggledEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val showNewContactAccountDialogListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.showNewContactAccountDialog = newValue - } - } - val showNewContactAccountDialog = MutableLiveData() - - val nativePresenceListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - if (newValue) { - if (PermissionHelper.get().hasWriteContactsPermission()) { - prefs.storePresenceInNativeContact = true - } else { - askWriteContactsPermissionForPresenceStorageEvent.value = Event(true) - } - } else { - prefs.storePresenceInNativeContact = false - } - } - } - val nativePresence = MutableLiveData() - - val showOrganizationListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.displayOrganization = newValue - } - } - val showOrganization = MutableLiveData() - - val launcherShortcutsListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.contactsShortcuts = newValue - launcherShortcutsEvent.value = Event(newValue) - } - } - val launcherShortcuts = MutableLiveData() - val launcherShortcutsEvent = MutableLiveData>() - - val ldapAvailable = MutableLiveData() - - val ldapConfigurations = MutableLiveData>() - - lateinit var ldapNewSettingsListener: SettingListenerStub - val ldapSettingsClickedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - private var ldapSettingsListener = object : SettingListenerStub() { - override fun onAccountClicked(identity: String) { - ldapSettingsClickedEvent.value = Event(identity.toInt()) - } - } - - init { - readContactsPermissionGranted.value = PermissionHelper.get().hasReadContactsPermission() - - friendListSubscribe.value = core.isFriendListSubscriptionEnabled - rlsAddressAvailable.value = !core.config.getString("sip", "rls_uri", "").isNullOrEmpty() - publishPresence.value = prefs.publishPresence - - showNewContactAccountDialog.value = prefs.showNewContactAccountDialog - nativePresence.value = prefs.storePresenceInNativeContact - showOrganization.value = prefs.displayOrganization - launcherShortcuts.value = prefs.contactsShortcuts - - ldapAvailable.value = core.ldapAvailable() - ldapConfigurations.value = arrayListOf() - - updateLdapConfigurationsList() - } - - fun updateLdapConfigurationsList() { - val list = arrayListOf() - var index = 0 - for (ldap in coreContext.core.ldapList) { - val viewModel = LdapSettingsViewModel(ldap, index.toString()) - viewModel.ldapSettingsListener = ldapSettingsListener - list.add(viewModel) - index += 1 - } - - ldapConfigurations.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/GenericSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/GenericSettingsViewModel.kt deleted file mode 100644 index 1313bbd40..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/GenericSettingsViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences - -abstract class GenericSettingsViewModel : ViewModel() { - protected val prefs = corePreferences - protected val core = coreContext.core -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/LdapSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/LdapSettingsViewModel.kt deleted file mode 100644 index b2fa74fbf..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/LdapSettingsViewModel.kt +++ /dev/null @@ -1,297 +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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import java.lang.NumberFormatException -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.core.Ldap -import org.linphone.core.tools.Log -import org.linphone.utils.Event - -class LdapSettingsViewModelFactory(private val index: Int) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - if (index >= 0 && index <= coreContext.core.ldapList.size) { - val ldap = coreContext.core.ldapList[index] - return LdapSettingsViewModel(ldap, index.toString()) as T - } - - val ldapParams = coreContext.core.createLdapParams() - val ldap = coreContext.core.createLdapWithParams(ldapParams) - return LdapSettingsViewModel(ldap, "-1") as T - } -} - -class LdapSettingsViewModel(private val ldap: Ldap, val index: String) : GenericSettingsViewModel() { - lateinit var ldapSettingsListener: SettingListenerStub - - val ldapConfigDeletedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val ldapEnableListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = ldap.params.clone() - params.enabled = newValue - ldap.params = params - } - } - val ldapEnable = MutableLiveData() - - val deleteListener = object : SettingListenerStub() { - override fun onClicked() { - coreContext.core.removeLdap(ldap) - ldapConfigDeletedEvent.value = Event(true) - } - } - - val ldapServerListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = ldap.params.clone() - params.server = newValue - ldap.params = params - } - } - val ldapServer = MutableLiveData() - - val ldapBindDnListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = ldap.params.clone() - params.bindDn = newValue - ldap.params = params - } - } - val ldapBindDn = MutableLiveData() - - val ldapPasswordListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = ldap.params.clone() - params.password = newValue - ldap.params = params - } - } - val ldapPassword = MutableLiveData() - - val ldapAuthMethodListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - val params = ldap.params.clone() - params.authMethod = Ldap.AuthMethod.fromInt(ldapAuthMethodValues[position]) - ldap.params = params - ldapAuthMethodIndex.value = position - } - } - val ldapAuthMethodIndex = MutableLiveData() - val ldapAuthMethodLabels = MutableLiveData>() - private val ldapAuthMethodValues = arrayListOf() - - val ldapTlsListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = ldap.params.clone() - params.isTlsEnabled = newValue - ldap.params = params - } - } - val ldapTls = MutableLiveData() - - val ldapCertCheckListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - val params = ldap.params.clone() - params.serverCertificatesVerificationMode = Ldap.CertVerificationMode.fromInt( - ldapCertCheckValues[position] - ) - ldap.params = params - ldapCertCheckIndex.value = position - } - } - val ldapCertCheckIndex = MutableLiveData() - val ldapCertCheckLabels = MutableLiveData>() - private val ldapCertCheckValues = arrayListOf() - - val ldapSearchBaseListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = ldap.params.clone() - params.baseObject = newValue - ldap.params = params - } - } - val ldapSearchBase = MutableLiveData() - - val ldapSearchFilterListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = ldap.params.clone() - params.filter = newValue - ldap.params = params - } - } - val ldapSearchFilter = MutableLiveData() - - val ldapSearchMaxResultsListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val intValue = newValue.toInt() - val params = ldap.params.clone() - params.maxResults = intValue - ldap.params = params - } catch (nfe: NumberFormatException) { - Log.e("[LDAP Settings] Failed to set max results ($newValue): $nfe") - } - } - } - val ldapSearchMaxResults = MutableLiveData() - - val ldapSearchTimeoutListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val intValue = newValue.toInt() - val params = ldap.params.clone() - params.timeout = intValue - ldap.params = params - } catch (nfe: NumberFormatException) { - Log.e("[LDAP Settings] Failed to set timeout ($newValue): $nfe") - } - } - } - val ldapSearchTimeout = MutableLiveData() - - val ldapRequestDelayListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val intValue = newValue.toInt() - val params = ldap.params.clone() - params.delay = intValue - ldap.params = params - } catch (nfe: NumberFormatException) { - Log.e("[LDAP Settings] Failed to set request delay ($newValue): $nfe") - } - } - } - val ldapRequestDelay = MutableLiveData() - - val ldapMinimumCharactersListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val intValue = newValue.toInt() - val params = ldap.params.clone() - params.minChars = intValue - ldap.params = params - } catch (nfe: NumberFormatException) { - Log.e("[LDAP Settings] Failed to set minimum characters ($newValue): $nfe") - } - } - } - val ldapMinimumCharacters = MutableLiveData() - - val ldapNameAttributeListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = ldap.params.clone() - params.nameAttribute = newValue - ldap.params = params - } - } - val ldapNameAttribute = MutableLiveData() - - val ldapSipAttributeListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = ldap.params.clone() - params.sipAttribute = newValue - ldap.params = params - } - } - val ldapSipAttribute = MutableLiveData() - - val ldapSipDomainListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val params = ldap.params.clone() - params.sipDomain = newValue - ldap.params = params - } - } - val ldapSipDomain = MutableLiveData() - - val ldapDebugListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val params = ldap.params.clone() - params.debugLevel = if (newValue) Ldap.DebugLevel.Verbose else Ldap.DebugLevel.Off - ldap.params = params - } - } - val ldapDebug = MutableLiveData() - - init { - val params = ldap.params - - ldapEnable.value = params.enabled - ldapServer.value = params.server - ldapBindDn.value = params.bindDn - ldapPassword.value = params.password - ldapTls.value = params.isTlsEnabled - ldapSearchBase.value = params.baseObject - ldapSearchFilter.value = params.filter - ldapSearchMaxResults.value = params.maxResults - ldapSearchTimeout.value = params.timeout - ldapRequestDelay.value = params.delay - ldapMinimumCharacters.value = params.minChars - ldapNameAttribute.value = params.nameAttribute - ldapSipAttribute.value = params.sipAttribute - ldapSipDomain.value = params.sipDomain - ldapDebug.value = params.debugLevel == Ldap.DebugLevel.Verbose - - initAuthMethodList() - initTlsCertCheckList() - } - - private fun initAuthMethodList() { - val labels = arrayListOf() - - labels.add(prefs.getString(R.string.contacts_settings_ldap_auth_method_anonymous)) - ldapAuthMethodValues.add(Ldap.AuthMethod.Anonymous.toInt()) - - labels.add(prefs.getString(R.string.contacts_settings_ldap_auth_method_simple)) - ldapAuthMethodValues.add(Ldap.AuthMethod.Simple.toInt()) - - ldapAuthMethodLabels.value = labels - ldapAuthMethodIndex.value = ldapAuthMethodValues.indexOf(ldap.params.authMethod.toInt()) - } - - private fun initTlsCertCheckList() { - val labels = arrayListOf() - - labels.add(prefs.getString(R.string.contacts_settings_ldap_cert_check_auto)) - ldapCertCheckValues.add(Ldap.CertVerificationMode.Default.toInt()) - - labels.add(prefs.getString(R.string.contacts_settings_ldap_cert_check_disabled)) - ldapCertCheckValues.add(Ldap.CertVerificationMode.Disabled.toInt()) - - labels.add(prefs.getString(R.string.contacts_settings_ldap_cert_check_enabled)) - ldapCertCheckValues.add(Ldap.CertVerificationMode.Enabled.toInt()) - - ldapCertCheckLabels.value = labels - ldapCertCheckIndex.value = ldapCertCheckValues.indexOf( - ldap.params.serverCertificatesVerificationMode.toInt() - ) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt deleted file mode 100644 index 057997971..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/NetworkSettingsViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.MutableLiveData -import java.lang.NumberFormatException -import org.linphone.activities.main.settings.SettingListenerStub - -class NetworkSettingsViewModel : GenericSettingsViewModel() { - val wifiOnlyListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isWifiOnlyEnabled = newValue - } - } - val wifiOnly = MutableLiveData() - - val allowIpv6Listener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isIpv6Enabled = newValue - } - } - val allowIpv6 = MutableLiveData() - - val randomPortsListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val port = if (newValue) -1 else 5060 - setTransportPort(port) - sipPort.value = port - } - } - val randomPorts = MutableLiveData() - - val sipPortListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val port = newValue.toInt() - setTransportPort(port) - } catch (_: NumberFormatException) { - } - } - } - val sipPort = MutableLiveData() - - init { - wifiOnly.value = core.isWifiOnlyEnabled - allowIpv6.value = core.isIpv6Enabled - randomPorts.value = getTransportPort() == -1 - sipPort.value = getTransportPort() - } - - private fun setTransportPort(port: Int) { - val transports = core.transports - transports.udpPort = port - transports.tcpPort = port - transports.tlsPort = -1 - core.transports = transports - } - - private fun getTransportPort(): Int { - val transports = core.transports - if (transports.udpPort > 0) return transports.udpPort - return transports.tcpPort - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt deleted file mode 100644 index c2c39fda9..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/SettingsViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.utils.LinphoneUtils - -class SettingsViewModel : ViewModel() { - private val tunnelAvailable: Boolean = coreContext.core.tunnelAvailable() - - val showAccountSettings: Boolean = corePreferences.showAccountSettings - val showTunnelSettings: Boolean = tunnelAvailable && corePreferences.showTunnelSettings - val showAudioSettings: Boolean = corePreferences.showAudioSettings - val showVideoSettings: Boolean = corePreferences.showVideoSettings - val showCallSettings: Boolean = corePreferences.showCallSettings - val showChatSettings: Boolean = corePreferences.showChatSettings - val showNetworkSettings: Boolean = corePreferences.showNetworkSettings - val showContactsSettings: Boolean = corePreferences.showContactsSettings - val showAdvancedSettings: Boolean = corePreferences.showAdvancedSettings - val showConferencesSettings: Boolean = corePreferences.showConferencesSettings - - val accounts = MutableLiveData>() - - private var accountClickListener = object : SettingListenerStub() { - override fun onAccountClicked(identity: String) { - accountsSettingsListener.onAccountClicked(identity) - } - } - - lateinit var accountsSettingsListener: SettingListenerStub - - lateinit var tunnelSettingsListener: SettingListenerStub - - lateinit var audioSettingsListener: SettingListenerStub - - lateinit var videoSettingsListener: SettingListenerStub - - lateinit var callSettingsListener: SettingListenerStub - - lateinit var chatSettingsListener: SettingListenerStub - - lateinit var networkSettingsListener: SettingListenerStub - - lateinit var contactsSettingsListener: SettingListenerStub - - lateinit var advancedSettingsListener: SettingListenerStub - - lateinit var conferencesSettingsListener: SettingListenerStub - - val primaryAccountDisplayNameListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val address = coreContext.core.createPrimaryContactParsed() - address ?: return - address.displayName = newValue - address.username = primaryAccountUsername.value - coreContext.core.primaryContact = address.asString() - - primaryAccountDisplayName.value = newValue - } - } - val primaryAccountDisplayName = MutableLiveData() - - val primaryAccountUsernameListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val address = coreContext.core.createPrimaryContactParsed() - address ?: return - address.username = newValue - address.displayName = primaryAccountDisplayName.value - coreContext.core.primaryContact = address.asString() - - primaryAccountUsername.value = newValue - } - } - val primaryAccountUsername = MutableLiveData() - - init { - updateAccountsList() - - val address = coreContext.core.createPrimaryContactParsed() - primaryAccountDisplayName.value = address?.displayName ?: "" - primaryAccountUsername.value = address?.username ?: "" - } - - override fun onCleared() { - accounts.value.orEmpty().forEach(AccountSettingsViewModel::destroy) - super.onCleared() - } - - fun updateAccountsList() { - accounts.value.orEmpty().forEach(AccountSettingsViewModel::destroy) - - val list = arrayListOf() - for (account in LinphoneUtils.getAccountsNotHidden()) { - val viewModel = AccountSettingsViewModel(account) - viewModel.accountsSettingsListener = accountClickListener - list.add(viewModel) - } - - accounts.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt deleted file mode 100644 index 8bdcf2110..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/TunnelSettingsViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.lifecycle.MutableLiveData -import java.lang.NumberFormatException -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.core.Factory -import org.linphone.core.Tunnel -import org.linphone.core.TunnelConfig - -class TunnelSettingsViewModel : GenericSettingsViewModel() { - val hostnameUrlListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val config = getTunnelConfig() - config.host = newValue - updateTunnelConfig(config) - } - } - val hostnameUrl = MutableLiveData() - - val portListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val config = getTunnelConfig() - config.port = newValue.toInt() - updateTunnelConfig(config) - } catch (_: NumberFormatException) { - } - } - } - val port = MutableLiveData() - - val useDualModeListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val tunnel = core.tunnel - tunnel?.isDualModeEnabled = newValue - } - } - val useDualMode = MutableLiveData() - - val hostnameUrl2Listener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - val config = getTunnelConfig() - config.host2 = newValue - updateTunnelConfig(config) - } - } - val hostnameUrl2 = MutableLiveData() - - val port2Listener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - val config = getTunnelConfig() - config.port2 = newValue.toInt() - updateTunnelConfig(config) - } catch (_: NumberFormatException) { - } - } - } - val port2 = MutableLiveData() - - val modeListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - core.tunnel?.mode = when (position) { - 0 -> Tunnel.Mode.Disable - 1 -> Tunnel.Mode.Enable - else -> Tunnel.Mode.Auto - } - } - } - val modeIndex = MutableLiveData() - val modeLabels = MutableLiveData>() - - init { - val tunnel = core.tunnel - val config = getTunnelConfig() - - hostnameUrl.value = config.host - port.value = config.port - useDualMode.value = tunnel?.isDualModeEnabled - hostnameUrl2.value = config.host2 - port2.value = config.port2 - - initModeList() - } - - private fun getTunnelConfig(): TunnelConfig { - val tunnel = core.tunnel - val configs = tunnel?.servers.orEmpty() - return if (configs.isNotEmpty()) { - configs.first() - } else { - Factory.instance().createTunnelConfig() - } - } - - private fun updateTunnelConfig(config: TunnelConfig) { - val tunnel = core.tunnel - tunnel?.cleanServers() - if (config.host?.isNotEmpty() == true) { - tunnel?.addServer(config) - } - } - - private fun initModeList() { - val labels = arrayListOf() - labels.add(prefs.getString(R.string.tunnel_settings_disabled_mode)) - labels.add(prefs.getString(R.string.tunnel_settings_always_mode)) - labels.add(prefs.getString(R.string.tunnel_settings_auto_mode)) - modeLabels.value = labels - - modeIndex.value = when (core.tunnel?.mode) { - Tunnel.Mode.Disable -> 0 - Tunnel.Mode.Enable -> 1 - else -> 2 - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt b/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt deleted file mode 100644 index db1db30ed..000000000 --- a/app/src/main/java/org/linphone/activities/main/settings/viewmodels/VideoSettingsViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.settings.viewmodels - -import androidx.databinding.ViewDataBinding -import androidx.lifecycle.MutableLiveData -import java.lang.NumberFormatException -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.core.Factory -import org.linphone.core.tools.Log - -class VideoSettingsViewModel : GenericSettingsViewModel() { - val enableVideoListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - core.isVideoCaptureEnabled = newValue - core.isVideoDisplayEnabled = newValue - if (!newValue) { - tabletPreview.value = false - initiateCall.value = false - autoAccept.value = false - } - } - } - val enableVideo = MutableLiveData() - - val tabletPreviewListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - prefs.videoPreview = newValue - } - } - val tabletPreview = MutableLiveData() - val isTablet = MutableLiveData() - - val initiateCallListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val policy = core.videoActivationPolicy - policy.automaticallyInitiate = newValue - core.videoActivationPolicy = policy - } - } - val initiateCall = MutableLiveData() - - val autoAcceptListener = object : SettingListenerStub() { - override fun onBoolValueChanged(newValue: Boolean) { - val policy = core.videoActivationPolicy - policy.automaticallyAccept = newValue - core.videoActivationPolicy = policy - } - } - val autoAccept = MutableLiveData() - - val cameraDeviceListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - core.videoDevice = cameraDeviceLabels.value.orEmpty()[position] - } - } - val cameraDeviceIndex = MutableLiveData() - val cameraDeviceLabels = MutableLiveData>() - - val videoSizeListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - core.setPreferredVideoDefinitionByName(videoSizeLabels.value.orEmpty()[position]) - } - } - val videoSizeIndex = MutableLiveData() - val videoSizeLabels = MutableLiveData>() - - val videoPresetListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - videoPresetIndex.value = position // Needed to display/hide two below settings - val currentPreset = core.videoPreset - val newPreset = videoPresetLabels.value.orEmpty()[position] - if (newPreset != currentPreset) { - if (currentPreset == "custom") { - // Not "custom" anymore, reset FPS & bandwidth - core.preferredFramerate = 0f - core.downloadBandwidth = 0 - core.uploadBandwidth = 0 - } - core.videoPreset = newPreset - } - } - } - val videoPresetIndex = MutableLiveData() - val videoPresetLabels = MutableLiveData>() - - val preferredFpsListener = object : SettingListenerStub() { - override fun onListValueChanged(position: Int) { - core.preferredFramerate = preferredFpsLabels.value.orEmpty()[position].toFloat() - } - } - val preferredFpsIndex = MutableLiveData() - val preferredFpsLabels = MutableLiveData>() - - val bandwidthLimitListener = object : SettingListenerStub() { - override fun onTextValueChanged(newValue: String) { - try { - core.downloadBandwidth = newValue.toInt() - core.uploadBandwidth = newValue.toInt() - } catch (_: NumberFormatException) { - } - } - } - val bandwidthLimit = MutableLiveData() - - val videoCodecs = MutableLiveData>() - - init { - enableVideo.value = core.isVideoEnabled && core.videoSupported() - tabletPreview.value = prefs.videoPreview - isTablet.value = coreContext.context.resources.getBoolean(R.bool.isTablet) - initiateCall.value = core.videoActivationPolicy.automaticallyInitiate - autoAccept.value = core.videoActivationPolicy.automaticallyAccept - - initCameraDevicesList() - initVideoSizeList() - initVideoPresetList() - initFpsList() - - bandwidthLimit.value = core.downloadBandwidth - } - - fun initCameraDevicesList() { - val labels = arrayListOf() - for (camera in core.videoDevicesList) { - if (prefs.hideStaticImageCamera && camera.startsWith("StaticImage")) { - Log.w("[Video Settings] Do not display StaticImage camera") - } else { - labels.add(camera) - } - } - - cameraDeviceLabels.value = labels - val index = labels.indexOf(core.videoDevice) - if (index == -1) { - val firstDevice = cameraDeviceLabels.value.orEmpty().firstOrNull() - Log.w( - "[Video Settings] Device not found in labels list: ${core.videoDevice}, replace it by $firstDevice" - ) - if (firstDevice != null) { - cameraDeviceIndex.value = 0 - core.videoDevice = firstDevice - } - } else { - cameraDeviceIndex.value = index - } - } - - private fun initVideoSizeList() { - val labels = arrayListOf() - - for (size in Factory.instance().supportedVideoDefinitions) { - labels.add(size.name.orEmpty()) - } - - videoSizeLabels.value = labels - videoSizeIndex.value = labels.indexOf(core.preferredVideoDefinition.name) - } - - private fun initVideoPresetList() { - val labels = arrayListOf() - - labels.add("default") - labels.add("high-fps") - labels.add("custom") - - videoPresetLabels.value = labels - videoPresetIndex.value = labels.indexOf(core.videoPreset) - } - - private fun initFpsList() { - val labels = arrayListOf("5", "10", "15", "20", "25", "30") - preferredFpsLabels.value = labels - preferredFpsIndex.value = labels.indexOf(core.preferredFramerate.toInt().toString()) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt b/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt deleted file mode 100644 index 1e73cef17..000000000 --- a/app/src/main/java/org/linphone/activities/main/sidemenu/fragments/SideMenuFragment.kt +++ /dev/null @@ -1,266 +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 . - */ -package org.linphone.activities.main.sidemenu.fragments - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.os.Parcelable -import android.provider.MediaStore -import android.view.View -import androidx.core.content.FileProvider -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import java.io.File -import kotlinx.coroutines.launch -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.MainActivity -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.activities.main.sidemenu.viewmodels.SideMenuViewModel -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.core.Factory -import org.linphone.core.tools.Log -import org.linphone.databinding.SideMenuFragmentBinding -import org.linphone.utils.* - -class SideMenuFragment : GenericFragment() { - private lateinit var viewModel: SideMenuViewModel - private var temporaryPicturePath: File? = null - - override fun getLayoutId(): Int = R.layout.side_menu_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - viewModel = ViewModelProvider(this)[SideMenuViewModel::class.java] - binding.viewModel = viewModel - - sharedViewModel.accountRemoved.observe( - viewLifecycleOwner - ) { - Log.i("[Side Menu] Account removed, update accounts list") - viewModel.updateAccountsList() - } - - sharedViewModel.defaultAccountChanged.observe( - viewLifecycleOwner - ) { - Log.i("[Side Menu] Default account changed, update accounts list") - viewModel.updateAccountsList() - } - - sharedViewModel.publishPresenceToggled.observe( - viewLifecycleOwner - ) { - viewModel.refreshConsolidatedPresence() - } - - viewModel.accountsSettingsListener = object : SettingListenerStub() { - override fun onAccountClicked(identity: String) { - Log.i("[Side Menu] Navigating to settings for account with identity: $identity") - - sharedViewModel.toggleDrawerEvent.value = Event(true) - - if (corePreferences.askForAccountPasswordToAccessSettings) { - showPasswordDialog(goToAccountSettings = true, accountIdentity = identity) - } else { - navigateToAccountSettings(identity) - } - } - } - - binding.setSelfPictureClickListener { - pickFile() - } - - binding.setAssistantClickListener { - sharedViewModel.toggleDrawerEvent.value = Event(true) - startActivity(Intent(context, AssistantActivity::class.java)) - } - - binding.setSettingsClickListener { - sharedViewModel.toggleDrawerEvent.value = Event(true) - - if (corePreferences.askForAccountPasswordToAccessSettings) { - showPasswordDialog(goToSettings = true) - } else { - navigateToSettings() - } - } - - binding.setRecordingsClickListener { - sharedViewModel.toggleDrawerEvent.value = Event(true) - navigateToRecordings() - } - - binding.setAboutClickListener { - sharedViewModel.toggleDrawerEvent.value = Event(true) - navigateToAbout() - } - - binding.setConferencesClickListener { - sharedViewModel.toggleDrawerEvent.value = Event(true) - navigateToScheduledConferences() - } - - binding.setQuitClickListener { - Log.i("[Side Menu] Quitting app") - requireActivity().finishAndRemoveTask() - - Log.i("[Side Menu] Stopping Core Context") - coreContext.notificationsManager.stopForegroundNotification() - coreContext.stop() - } - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (resultCode == Activity.RESULT_OK) { - lifecycleScope.launch { - val contactImageFilePath = FileUtils.getFilePathFromPickerIntent( - data, - temporaryPicturePath - ) - if (contactImageFilePath != null) { - viewModel.setPictureFromPath(contactImageFilePath) - } - } - } - } - - private fun pickFile() { - val cameraIntents = ArrayList() - - // Handles image picking - val galleryIntent = Intent(Intent.ACTION_PICK) - galleryIntent.type = "image/*" - - if (PermissionHelper.get().hasCameraPermission()) { - // Allows to capture directly from the camera - val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - val tempFileName = System.currentTimeMillis().toString() + ".jpeg" - val file = FileUtils.getFileStoragePath(tempFileName) - temporaryPicturePath = file - val publicUri = FileProvider.getUriForFile( - requireContext(), - requireContext().getString(R.string.file_provider), - file - ) - captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, publicUri) - captureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - cameraIntents.add(captureIntent) - } - - val chooserIntent = - Intent.createChooser(galleryIntent, getString(R.string.chat_message_pick_file_dialog)) - chooserIntent.putExtra( - Intent.EXTRA_INITIAL_INTENTS, - cameraIntents.toArray(arrayOf()) - ) - - startActivityForResult(chooserIntent, 0) - } - - private fun showPasswordDialog( - goToSettings: Boolean = false, - goToAccountSettings: Boolean = false, - accountIdentity: String = "" - ) { - val dialogViewModel = DialogViewModel( - getString(R.string.settings_password_protection_dialog_title) - ) - dialogViewModel.showIcon = true - dialogViewModel.iconResource = R.drawable.security_toggle_icon_green - dialogViewModel.showPassword = true - dialogViewModel.passwordTitle = getString( - R.string.settings_password_protection_dialog_input_hint - ) - val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel) - - dialogViewModel.showCancelButton { - dialog.dismiss() - } - - dialogViewModel.showOkButton( - { - val defaultAccount = coreContext.core.defaultAccount ?: coreContext.core.accountList.firstOrNull() - if (defaultAccount == null) { - Log.e("[Side Menu] No account found, can't check password input!") - (requireActivity() as MainActivity).showSnackBar(R.string.error_unexpected) - } else { - val authInfo = defaultAccount.findAuthInfo() - if (authInfo == null) { - Log.e( - "[Side Menu] No auth info found for account [${defaultAccount.params.identityAddress?.asString()}], can't check password input!" - ) - (requireActivity() as MainActivity).showSnackBar(R.string.error_unexpected) - } else { - val expectedHash = authInfo.ha1 - if (expectedHash == null) { - Log.e( - "[Side Menu] No ha1 found in auth info, can't check password input!" - ) - (requireActivity() as MainActivity).showSnackBar( - R.string.error_unexpected - ) - } else { - val hashAlgorithm = authInfo.algorithm ?: "MD5" - val userId = (authInfo.userid ?: authInfo.username).orEmpty() - val realm = authInfo.realm.orEmpty() - val password = dialogViewModel.password - val computedHash = Factory.instance().computeHa1ForAlgorithm( - userId, - password, - realm, - hashAlgorithm - ) - if (computedHash != expectedHash) { - Log.e( - "[Side Menu] Computed hash [$computedHash] using userId [$userId], realm [$realm] and algorithm [$hashAlgorithm] doesn't match expected hash!" - ) - (requireActivity() as MainActivity).showSnackBar( - R.string.settings_password_protection_dialog_invalid_input - ) - } else { - if (goToSettings) { - navigateToSettings() - } else if (goToAccountSettings) { - navigateToAccountSettings(accountIdentity) - } - } - } - } - } - - dialog.dismiss() - }, - getString(R.string.settings_password_protection_dialog_ok_label) - ) - - dialog.show() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt b/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt deleted file mode 100644 index 1f07fedb6..000000000 --- a/app/src/main/java/org/linphone/activities/main/sidemenu/viewmodels/SideMenuViewModel.kt +++ /dev/null @@ -1,128 +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 . - */ -package org.linphone.activities.main.sidemenu.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.activities.main.settings.SettingListenerStub -import org.linphone.activities.main.settings.viewmodels.AccountSettingsViewModel -import org.linphone.core.* -import org.linphone.utils.LinphoneUtils - -class SideMenuViewModel : ViewModel() { - val showAccounts: Boolean = corePreferences.showAccountsInSideMenu - val showAssistant: Boolean = corePreferences.showAssistantInSideMenu - val showSettings: Boolean = corePreferences.showSettingsInSideMenu - val showRecordings: Boolean = corePreferences.showRecordingsInSideMenu - val showScheduledConferences = MutableLiveData() - val showAbout: Boolean = corePreferences.showAboutInSideMenu - val showQuit: Boolean = corePreferences.showQuitInSideMenu - - val defaultAccountViewModel = MutableLiveData() - val defaultAccountFound = MutableLiveData() - val defaultAccountAvatar = MutableLiveData() - - val accounts = MutableLiveData>() - - val presenceStatus = MutableLiveData() - - lateinit var accountsSettingsListener: SettingListenerStub - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onAccountRegistrationStateChanged( - core: Core, - account: Account, - state: RegistrationState, - message: String - ) { - // +1 is for the default account, otherwise this will trigger every time - if (accounts.value.isNullOrEmpty() || - coreContext.core.accountList.size != accounts.value.orEmpty().size + 1 - ) { - // Only refresh the list if an account has been added or removed - updateAccountsList() - } - } - } - - init { - defaultAccountFound.value = false - defaultAccountAvatar.value = corePreferences.defaultAccountAvatarPath - showScheduledConferences.value = corePreferences.showScheduledConferencesInSideMenu && - LinphoneUtils.isRemoteConferencingAvailable() - coreContext.core.addListener(listener) - updateAccountsList() - refreshConsolidatedPresence() - } - - override fun onCleared() { - defaultAccountViewModel.value?.destroy() - accounts.value.orEmpty().forEach(AccountSettingsViewModel::destroy) - coreContext.core.removeListener(listener) - super.onCleared() - } - - fun refreshConsolidatedPresence() { - presenceStatus.value = coreContext.core.consolidatedPresence - } - - fun updateAccountsList() { - defaultAccountFound.value = false // Do not assume a default account will still be found - defaultAccountViewModel.value?.destroy() - accounts.value.orEmpty().forEach(AccountSettingsViewModel::destroy) - - val list = arrayListOf() - val defaultAccount = coreContext.core.defaultAccount - if (defaultAccount != null) { - val defaultViewModel = AccountSettingsViewModel(defaultAccount) - defaultViewModel.accountsSettingsListener = object : SettingListenerStub() { - override fun onAccountClicked(identity: String) { - accountsSettingsListener.onAccountClicked(identity) - } - } - defaultAccountViewModel.value = defaultViewModel - defaultAccountFound.value = true - } - - for (account in LinphoneUtils.getAccountsNotHidden()) { - if (account != coreContext.core.defaultAccount) { - val viewModel = AccountSettingsViewModel(account) - viewModel.accountsSettingsListener = object : SettingListenerStub() { - override fun onAccountClicked(identity: String) { - accountsSettingsListener.onAccountClicked(identity) - } - } - list.add(viewModel) - } - } - accounts.value = list - - showScheduledConferences.value = corePreferences.showScheduledConferencesInSideMenu && - LinphoneUtils.isRemoteConferencingAvailable() - } - - fun setPictureFromPath(picturePath: String) { - corePreferences.defaultAccountAvatarPath = picturePath - defaultAccountAvatar.value = corePreferences.defaultAccountAvatarPath - coreContext.contactsManager.updateLocalContacts() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/CallOverlayViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/CallOverlayViewModel.kt deleted file mode 100644 index 79863593f..000000000 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/CallOverlayViewModel.kt +++ /dev/null @@ -1,79 +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 . - */ -package org.linphone.activities.main.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.core.Call -import org.linphone.core.Core -import org.linphone.core.CoreListenerStub -import org.linphone.core.tools.Log - -class CallOverlayViewModel : ViewModel() { - val displayCallOverlay = MutableLiveData() - - private val listener = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State?, - message: String - ) { - if (core.callsNb == 1 && call.state == Call.State.Connected) { - Log.i("[Call Overlay] First call connected, creating it") - createCallOverlay() - } - } - - override fun onLastCallEnded(core: Core) { - Log.i("[Call Overlay] Last call ended, removing it") - removeCallOverlay() - } - } - - init { - displayCallOverlay.value = corePreferences.showCallOverlay && - !corePreferences.systemWideCallOverlay && - coreContext.core.callsNb > 0 - - coreContext.core.addListener(listener) - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - - super.onCleared() - } - - private fun createCallOverlay() { - // If overlay is disabled or if system-wide call overlay is enabled, abort - if (!corePreferences.showCallOverlay || corePreferences.systemWideCallOverlay) { - return - } - - displayCallOverlay.value = true - } - - private fun removeCallOverlay() { - displayCallOverlay.value = false - } -} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt deleted file mode 100644 index 3cc7440d5..000000000 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/DialogViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.R -import org.linphone.utils.AppUtils -import org.linphone.utils.Event - -class DialogViewModel(val message: String, val title: String = "") : ViewModel() { - var showDoNotAskAgain: Boolean = false - - var showZrtp: Boolean = false - - var zrtpReadSas: String = "" - - var zrtpListenSas: String = "" - - var showTitle: Boolean = false - - var showIcon: Boolean = false - - var iconResource: Int = 0 - - var showSubscribeLinphoneOrgLink: Boolean = false - - val doNotAskAgain = MutableLiveData() - - val dismissEvent = MutableLiveData>() - - var password: String = "" - - var passwordTitle: String = "" - - var passwordSubtitle: String = "" - - var showPassword: Boolean = false - - init { - doNotAskAgain.value = false - showTitle = title.isNotEmpty() - } - - var showCancel: Boolean = false - var cancelLabel: String = AppUtils.getString(R.string.dialog_cancel) - private var onCancel: (Boolean) -> Unit = {} - - fun showCancelButton(cancel: (Boolean) -> Unit) { - showCancel = true - onCancel = cancel - } - - fun showCancelButton(cancel: (Boolean) -> Unit, label: String = cancelLabel) { - showCancel = true - onCancel = cancel - cancelLabel = label - } - - fun onCancelClicked() { - onCancel(doNotAskAgain.value == true) - } - - var showDelete: Boolean = false - var deleteLabel: String = AppUtils.getString(R.string.dialog_delete) - private var onDelete: (Boolean) -> Unit = {} - - fun showDeleteButton(delete: (Boolean) -> Unit, label: String) { - showDelete = true - onDelete = delete - deleteLabel = label - } - - fun onDeleteClicked() { - onDelete(doNotAskAgain.value == true) - } - - var showOk: Boolean = false - var okLabel: String = AppUtils.getString(R.string.dialog_ok) - private var onOk: (Boolean) -> Unit = {} - - fun showOkButton(ok: (Boolean) -> Unit, label: String = okLabel) { - showOk = true - onOk = ok - okLabel = label - } - - fun onOkClicked() { - onOk(doNotAskAgain.value == true) - } - - fun dismiss() { - dismissEvent.value = Event(true) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt deleted file mode 100644 index 0c0ffc090..000000000 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/ListTopBarViewModel.kt +++ /dev/null @@ -1,82 +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 . - */ -package org.linphone.activities.main.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.utils.Event - -/** - * This view model is dedicated to the top bar while in edition mode for item(s) selection in list - */ -class ListTopBarViewModel : ViewModel() { - val isEditionEnabled = MutableLiveData() - - val isSelectionNotEmpty = MutableLiveData() - - val selectAllEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val unSelectAllEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val deleteSelectionEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val selectedItems = MutableLiveData>() - - init { - isEditionEnabled.value = false - isSelectionNotEmpty.value = false - selectedItems.value = arrayListOf() - } - - fun onSelectAll(lastIndex: Int) { - val list = arrayListOf() - list.addAll(0.rangeTo(lastIndex)) - - selectedItems.value = list - isSelectionNotEmpty.value = list.isNotEmpty() - } - - fun onUnSelectAll() { - val list = arrayListOf() - - selectedItems.value = list - isSelectionNotEmpty.value = false - } - - fun onToggleSelect(position: Int) { - val list = arrayListOf() - list.addAll(selectedItems.value.orEmpty()) - - if (list.contains(position)) { - list.remove(position) - } else { - list.add(position) - } - - isSelectionNotEmpty.value = list.isNotEmpty() - selectedItems.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/LogsUploadViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/LogsUploadViewModel.kt deleted file mode 100644 index ca9a083df..000000000 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/LogsUploadViewModel.kt +++ /dev/null @@ -1,80 +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 . - */ - -package org.linphone.activities.main.viewmodels - -import androidx.lifecycle.MutableLiveData -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.Core -import org.linphone.core.CoreListenerStub -import org.linphone.utils.Event - -open class LogsUploadViewModel : MessageNotifierViewModel() { - val uploadInProgress = MutableLiveData() - - val resetCompleteEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val uploadFinishedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val uploadErrorEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val listener = object : CoreListenerStub() { - override fun onLogCollectionUploadStateChanged( - core: Core, - state: Core.LogCollectionUploadState, - info: String - ) { - if (state == Core.LogCollectionUploadState.Delivered) { - uploadInProgress.value = false - uploadFinishedEvent.value = Event(info) - } else if (state == Core.LogCollectionUploadState.NotDelivered) { - uploadInProgress.value = false - uploadErrorEvent.value = Event(true) - } - } - } - - init { - coreContext.core.addListener(listener) - uploadInProgress.value = false - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - - super.onCleared() - } - - fun uploadLogs() { - uploadInProgress.value = true - coreContext.core.uploadLogCollection() - } - - fun resetLogs() { - coreContext.core.resetLogCollection() - resetCompleteEvent.value = Event(true) - } -} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/MessageNotifierViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/MessageNotifierViewModel.kt deleted file mode 100644 index 6e81ee8db..000000000 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/MessageNotifierViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.utils.Event - -/* Helper for view models to notify user of a massage through a Snackbar */ -abstract class MessageNotifierViewModel : ViewModel() { - val onMessageToNotifyEvent = MutableLiveData>() -} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/SharedMainViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/SharedMainViewModel.kt deleted file mode 100644 index 5bc6ae662..000000000 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/SharedMainViewModel.kt +++ /dev/null @@ -1,121 +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 . - */ -package org.linphone.activities.main.viewmodels - -import android.net.Uri -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.activities.main.history.data.GroupedCallLogData -import org.linphone.core.* -import org.linphone.utils.Event - -class SharedMainViewModel : ViewModel() { - val toggleDrawerEvent = MutableLiveData>() - - val layoutChangedEvent = MutableLiveData>() - var isSlidingPaneSlideable = MutableLiveData() - - /* Call history */ - - val selectedCallLogGroup = MutableLiveData() - - /* Chat */ - - val selectedChatRoom = MutableLiveData() - var destructionPendingChatRoom: ChatRoom? = null - - val selectedGroupChatRoom = MutableLiveData() - - val filesToShare = MutableLiveData>() - - val textToShare = MutableLiveData() - - val messageToForwardEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val isPendingMessageForward = MutableLiveData() - - val contentToOpen = MutableLiveData() - - var createEncryptedChatRoom: Boolean = corePreferences.forceEndToEndEncryptedChat - - val chatRoomParticipants = MutableLiveData>() - - var chatRoomSubject: String = "" - - // When using keyboard to share gif or other, see RichContentReceiver & RichEditText classes - val richContentUri = MutableLiveData>() - - val refreshChatRoomInListEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - /* Contacts */ - - val selectedContact = MutableLiveData() - - // For correct animations directions - val updateContactsAnimationsBasedOnDestination: MutableLiveData> by lazy { - MutableLiveData>() - } - - /* Accounts */ - - val defaultAccountChanged: MutableLiveData by lazy { - MutableLiveData() - } - - val accountRemoved: MutableLiveData by lazy { - MutableLiveData() - } - - val publishPresenceToggled: MutableLiveData by lazy { - MutableLiveData() - } - - val accountSettingsFragmentOpenedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - /* Call */ - - var pendingCallTransfer: Boolean = false - - /* Conference */ - - val addressOfConferenceInfoToEdit: MutableLiveData> by lazy { - MutableLiveData>() - } - - val participantsListForNextScheduledMeeting: MutableLiveData>> by lazy { - MutableLiveData>>() - } - - /* Dialer */ - - var dialerUri: String = "" - - // For correct animations directions - val updateDialerAnimationsBasedOnDestination: MutableLiveData> by lazy { - MutableLiveData>() - } -} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt deleted file mode 100644 index 7b10b5210..000000000 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/StatusViewModel.kt +++ /dev/null @@ -1,118 +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 . - */ -package org.linphone.activities.main.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import java.util.* -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.core.* -import org.linphone.core.tools.Log - -open class StatusViewModel : ViewModel() { - val registrationStatusText = MutableLiveData() - - val registrationStatusDrawable = MutableLiveData() - - val voiceMailCount = MutableLiveData() - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onAccountRegistrationStateChanged( - core: Core, - account: Account, - state: RegistrationState, - message: String - ) { - if (account == core.defaultAccount) { - updateDefaultAccountRegistrationStatus(state) - } else if (core.accountList.isEmpty()) { - // Update registration status when default account is removed - registrationStatusText.value = getStatusIconText(state) - registrationStatusDrawable.value = getStatusIconResource(state) - } - } - - override fun onNotifyReceived( - core: Core, - event: Event, - notifiedEvent: String, - body: Content? - ) { - if (body?.type == "application" && body.subtype == "simple-message-summary" && body.size > 0) { - val data = body.utf8Text?.lowercase(Locale.getDefault()) - val voiceMail = data?.split("voice-message: ") - if ((voiceMail?.size ?: 0) >= 2) { - val toParse = voiceMail!![1].split("/", limit = 0) - try { - val unreadCount: Int = toParse[0].toInt() - voiceMailCount.value = unreadCount - } catch (nfe: NumberFormatException) { - Log.e("[Status Fragment] $nfe") - } - } - } - } - } - - init { - val core = coreContext.core - core.addListener(listener) - - var state: RegistrationState = RegistrationState.None - val defaultAccount = core.defaultAccount - if (defaultAccount != null) { - state = defaultAccount.state - } - updateDefaultAccountRegistrationStatus(state) - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - super.onCleared() - } - - fun refreshRegister() { - coreContext.core.refreshRegisters() - } - - fun updateDefaultAccountRegistrationStatus(state: RegistrationState) { - registrationStatusText.value = getStatusIconText(state) - registrationStatusDrawable.value = getStatusIconResource(state) - } - - private fun getStatusIconText(state: RegistrationState): Int { - return when (state) { - RegistrationState.Ok -> R.string.status_connected - RegistrationState.Progress, RegistrationState.Refreshing -> R.string.status_in_progress - RegistrationState.Failed -> R.string.status_error - else -> R.string.status_not_connected - } - } - - private fun getStatusIconResource(state: RegistrationState): Int { - return when (state) { - RegistrationState.Ok -> R.drawable.led_registered - RegistrationState.Progress, RegistrationState.Refreshing -> R.drawable.led_registration_in_progress - RegistrationState.Failed -> R.drawable.led_error - else -> R.drawable.led_not_registered - } - } -} diff --git a/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt b/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt deleted file mode 100644 index f637c71a3..000000000 --- a/app/src/main/java/org/linphone/activities/main/viewmodels/TabsViewModel.kt +++ /dev/null @@ -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 . - */ -package org.linphone.activities.main.viewmodels - -import android.animation.ValueAnimator -import android.view.animation.LinearInterpolator -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.core.* -import org.linphone.utils.AppUtils - -class TabsViewModel : ViewModel() { - val unreadMessagesCount = MutableLiveData() - val missedCallsCount = MutableLiveData() - - val leftAnchor = MutableLiveData() - val middleAnchor = MutableLiveData() - val rightAnchor = MutableLiveData() - - val historyMissedCountTranslateY = MutableLiveData() - val chatUnreadCountTranslateY = MutableLiveData() - - 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 - historyMissedCountTranslateY.value = -value - chatUnreadCountTranslateY.value = -value - } - interpolator = LinearInterpolator() - duration = 250 - repeatMode = ValueAnimator.REVERSE - repeatCount = ValueAnimator.INFINITE - } - } - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - if (state == Call.State.End || state == Call.State.Error) { - updateMissedCallCount() - } - } - - override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { - updateUnreadChatCount() - } - - override fun onMessagesReceived( - core: Core, - chatRoom: ChatRoom, - messages: Array - ) { - updateUnreadChatCount() - } - - override fun onChatRoomStateChanged(core: Core, chatRoom: ChatRoom, state: ChatRoom.State) { - if (state == ChatRoom.State.Deleted) { - updateUnreadChatCount() - } - } - } - - init { - coreContext.core.addListener(listener) - - if (corePreferences.disableChat) { - leftAnchor.value = 1 / 3F - middleAnchor.value = 2 / 3F - rightAnchor.value = 1F - } else { - leftAnchor.value = 0.25F - middleAnchor.value = 0.5F - rightAnchor.value = 0.75F - } - - updateUnreadChatCount() - updateMissedCallCount() - - if (corePreferences.enableAnimations) bounceAnimator.start() - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - super.onCleared() - } - - fun updateMissedCallCount() { - missedCallsCount.value = coreContext.core.missedCallsCount - } - - fun updateUnreadChatCount() { - unreadMessagesCount.value = if (corePreferences.disableChat) 0 else coreContext.core.unreadChatMessageCountFromActiveLocals - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/CallActivity.kt b/app/src/main/java/org/linphone/activities/voip/CallActivity.kt deleted file mode 100644 index 089bf912f..000000000 --- a/app/src/main/java/org/linphone/activities/voip/CallActivity.kt +++ /dev/null @@ -1,331 +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 . - */ -package org.linphone.activities.voip - -import android.Manifest -import android.content.Intent -import android.content.pm.PackageManager -import android.content.res.Configuration -import android.os.Build -import android.os.Bundle -import androidx.annotation.RequiresApi -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.findNavController -import androidx.window.layout.FoldingFeature -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.* -import org.linphone.activities.main.MainActivity -import org.linphone.activities.voip.viewmodels.CallsViewModel -import org.linphone.activities.voip.viewmodels.ConferenceViewModel -import org.linphone.activities.voip.viewmodels.ControlsViewModel -import org.linphone.activities.voip.viewmodels.StatisticsListViewModel -import org.linphone.compatibility.Compatibility -import org.linphone.core.Call -import org.linphone.core.GlobalState -import org.linphone.core.tools.Log -import org.linphone.databinding.VoipActivityBinding -import org.linphone.mediastream.Version -import org.linphone.utils.PermissionHelper - -class CallActivity : ProximitySensorActivity() { - private lateinit var binding: VoipActivityBinding - private lateinit var controlsViewModel: ControlsViewModel - private lateinit var callsViewModel: CallsViewModel - private lateinit var conferenceViewModel: ConferenceViewModel - private lateinit var statsViewModel: StatisticsListViewModel - - override fun onCreate(savedInstanceState: Bundle?) { - // Flag in manifest should be enough starting Android 8.1 - if (Version.sdkStrictlyBelow(Version.API27_OREO_81)) { - Compatibility.setShowWhenLocked(this, true) - Compatibility.setTurnScreenOn(this, true) - Compatibility.requestDismissKeyguard(this) - } - - super.onCreate(savedInstanceState) - - binding = DataBindingUtil.setContentView(this, R.layout.voip_activity) - binding.lifecycleOwner = this - } - - override fun onPostCreate(savedInstanceState: Bundle?) { - super.onPostCreate(savedInstanceState) - - // This can't be done in onCreate(), has to be at least in onPostCreate() ! - val navController = binding.navHostFragment.findNavController() - val navControllerStoreOwner = navController.getViewModelStoreOwner(R.id.call_nav_graph) - - controlsViewModel = ViewModelProvider(navControllerStoreOwner)[ControlsViewModel::class.java] - binding.controlsViewModel = controlsViewModel - - callsViewModel = ViewModelProvider(navControllerStoreOwner)[CallsViewModel::class.java] - - conferenceViewModel = ViewModelProvider(navControllerStoreOwner)[ConferenceViewModel::class.java] - - statsViewModel = ViewModelProvider(navControllerStoreOwner)[StatisticsListViewModel::class.java] - - val isInPipMode = Compatibility.isInPictureInPictureMode(this) - Log.i("[Call Activity] onPostCreate: is in PiP mode? $isInPipMode") - controlsViewModel.pipMode.value = isInPipMode - - controlsViewModel.askPermissionEvent.observe( - this - ) { - it.consume { permission -> - Log.i("[Call Activity] Asking for $permission permission") - requestPermissions(arrayOf(permission), 0) - } - } - - controlsViewModel.fullScreenMode.observe( - this - ) { hide -> - Compatibility.hideAndroidSystemUI(hide, window) - } - - controlsViewModel.proximitySensorEnabled.observe( - this - ) { enabled -> - Log.i( - "[Call Activity] ${if (enabled) "Enabling" else "Disabling"} proximity sensor (if possible)" - ) - enableProximitySensor(enabled) - } - - controlsViewModel.isVideoEnabled.observe( - this - ) { enabled -> - Compatibility.enableAutoEnterPiP( - this, - enabled, - conferenceViewModel.conferenceExists.value == true - ) - } - - controlsViewModel.callStatsVisible.observe( - this - ) { visible -> - if (visible) statsViewModel.enable() else statsViewModel.disable() - } - - callsViewModel.noMoreCallEvent.observe( - this - ) { - it.consume { noMoreCall -> - if (noMoreCall) { - Log.i("[Call Activity] No more call event fired, finishing activity") - finish() - } - } - } - - callsViewModel.currentCallData.observe( - this - ) { callData -> - val call = callData.call - if (call.conference == null) { - Log.i( - "[Call Activity] Current call isn't linked to a conference, switching to SingleCall fragment" - ) - navigateToActiveCall() - } else { - Log.i( - "[Call Activity] Current call is linked to a conference, switching to ConferenceCall fragment" - ) - navigateToConferenceCall() - } - } - - callsViewModel.askPermissionEvent.observe( - this - ) { - it.consume { permission -> - Log.i("[Call Activity] Asking for $permission permission") - requestPermissions(arrayOf(permission), 0) - } - } - - conferenceViewModel.conferenceExists.observe( - this - ) { exists -> - if (exists) { - Log.i( - "[Call Activity] Found active conference, changing switching to ConferenceCall fragment" - ) - navigateToConferenceCall() - } else if (coreContext.core.callsNb > 0) { - Log.i( - "[Call Activity] Conference no longer exists, switching to SingleCall fragment" - ) - navigateToActiveCall() - } - } - - conferenceViewModel.isConferenceLocallyPaused.observe( - this - ) { paused -> - if (!paused) { - Log.i("[Call Activity] Entered conference, make sure conference fragment is active") - navigateToConferenceCall() - } - } - - checkPermissions() - } - - override fun onUserLeaveHint() { - super.onUserLeaveHint() - - if (coreContext.core.currentCall?.currentParams?.isVideoEnabled == true) { - Log.i("[Call Activity] Entering PiP mode") - Compatibility.enterPipMode(this, conferenceViewModel.conferenceExists.value == true) - } - } - - @RequiresApi(Build.VERSION_CODES.O) - override fun onPictureInPictureModeChanged( - isInPictureInPictureMode: Boolean, - newConfig: Configuration - ) { - super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) - - Log.i( - "[Call Activity] onPictureInPictureModeChanged: is in PiP mode? $isInPictureInPictureMode" - ) - if (::controlsViewModel.isInitialized) { - // To hide UI except for TextureViews - controlsViewModel.pipMode.value = isInPictureInPictureMode - } - } - - override fun onResume() { - super.onResume() - - if (coreContext.core.callsNb == 0) { - Log.w("[Call Activity] Resuming but no call found...") - if (isTaskRoot) { - // When resuming app from recent tasks make sure MainActivity will be launched if there is no call - val intent = Intent() - intent.setClass(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - startActivity(intent) - } else { - finish() - } - return - } - coreContext.removeCallOverlay() - - val currentCall = coreContext.core.currentCall - when (currentCall?.state) { - Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia, Call.State.OutgoingProgress, Call.State.OutgoingRinging -> { - navigateToOutgoingCall() - } - Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> { - val earlyMediaVideoEnabled = corePreferences.acceptEarlyMedia && - currentCall.state == Call.State.IncomingEarlyMedia && - currentCall.currentParams.isVideoEnabled - navigateToIncomingCall(earlyMediaVideoEnabled) - } - else -> {} - } - } - - override fun onPause() { - val core = coreContext.core - if (core.callsNb > 0) { - coreContext.createCallOverlay() - } - - super.onPause() - } - - override fun onDestroy() { - if (coreContext.core.globalState != GlobalState.Off) { - coreContext.core.nativeVideoWindowId = null - coreContext.core.nativePreviewWindowId = null - } - - super.onDestroy() - } - - private fun checkPermissions() { - val permissionsRequiredList = arrayListOf() - - if (!PermissionHelper.get().hasRecordAudioPermission()) { - Log.i("[Call Activity] Asking for RECORD_AUDIO permission") - permissionsRequiredList.add(Manifest.permission.RECORD_AUDIO) - } - - if (callsViewModel.currentCallData.value?.call?.currentParams?.isVideoEnabled == true && - !PermissionHelper.get().hasCameraPermission() - ) { - Log.i("[Call Activity] Asking for CAMERA permission") - permissionsRequiredList.add(Manifest.permission.CAMERA) - } - - if (permissionsRequiredList.isNotEmpty()) { - val permissionsRequired = arrayOfNulls(permissionsRequiredList.size) - permissionsRequiredList.toArray(permissionsRequired) - requestPermissions(permissionsRequired, 0) - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == 0) { - for (i in permissions.indices) { - when (permissions[i]) { - Manifest.permission.RECORD_AUDIO -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Call Activity] RECORD_AUDIO permission has been granted") - callsViewModel.updateMicState() - } - Manifest.permission.CAMERA -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Log.i("[Call Activity] CAMERA permission has been granted") - coreContext.core.reloadVideoDevices() - controlsViewModel.toggleVideo() - } - Manifest.permission.WRITE_EXTERNAL_STORAGE -> if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { - Log.i( - "[Call Activity] WRITE_EXTERNAL_STORAGE permission has been granted, taking snapshot" - ) - controlsViewModel.takeSnapshot() - } - } - } - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - } - - override fun onLayoutChanges(foldingFeature: FoldingFeature?) { - foldingFeature ?: return - Log.i( - "[Call Activity] Folding feature state changed: ${foldingFeature.state}, orientation is ${foldingFeature.orientation}" - ) - - controlsViewModel.foldingState.value = foldingFeature - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/ConferenceDisplayMode.kt b/app/src/main/java/org/linphone/activities/voip/ConferenceDisplayMode.kt deleted file mode 100644 index 0e529b351..000000000 --- a/app/src/main/java/org/linphone/activities/voip/ConferenceDisplayMode.kt +++ /dev/null @@ -1,26 +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 . - */ -package org.linphone.activities.voip - -enum class ConferenceDisplayMode { - GRID, - ACTIVE_SPEAKER, - AUDIO_ONLY -} diff --git a/app/src/main/java/org/linphone/activities/voip/data/CallData.kt b/app/src/main/java/org/linphone/activities/voip/data/CallData.kt deleted file mode 100644 index 4c2fe15db..000000000 --- a/app/src/main/java/org/linphone/activities/voip/data/CallData.kt +++ /dev/null @@ -1,315 +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 . - */ -package org.linphone.activities.voip.data - -import android.view.View -import android.widget.Toast -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import java.util.* -import kotlinx.coroutines.* -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.compatibility.Compatibility -import org.linphone.contact.GenericContactData -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils -import org.linphone.utils.LinphoneUtils - -open class CallData(val call: Call) : GenericContactData(call.remoteAddress) { - interface CallContextMenuClickListener { - fun onShowContextMenu(anchor: View, callData: CallData) - } - - val displayableAddress = MutableLiveData() - - val isPaused = MutableLiveData() - val isRemotelyPaused = MutableLiveData() - val canBePaused = MutableLiveData() - - val isRecording = MutableLiveData() - val isRemotelyRecorded = MutableLiveData() - - val isInRemoteConference = MutableLiveData() - val remoteConferenceSubject = MutableLiveData() - val isConferenceCall = MediatorLiveData() - val conferenceParticipants = MutableLiveData>() - val conferenceParticipantsCountLabel = MutableLiveData() - - val isOutgoing = MutableLiveData() - val isIncoming = MutableLiveData() - - var chatRoom: ChatRoom? = null - - var contextMenuClickListener: CallContextMenuClickListener? = null - - private var timer: Timer? = null - - private val listener = object : CallListenerStub() { - override fun onStateChanged(call: Call, state: Call.State, message: String) { - if (call != this@CallData.call) return - Log.i("[Call] State changed: $state") - - update() - - if (call.state == Call.State.UpdatedByRemote) { - val remoteVideo = call.remoteParams?.isVideoEnabled ?: false - val localVideo = call.currentParams.isVideoEnabled - if (remoteVideo && !localVideo) { - // User has 30 secs to accept or decline call update - startVideoUpdateAcceptanceTimer() - } - } else if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { - timer?.cancel() - } else if (state == Call.State.StreamsRunning) { - // Stop call update timer once user has accepted or declined call update - timer?.cancel() - } - } - - override fun onRemoteRecording(call: Call, recording: Boolean) { - Log.i("[Call] Remote recording changed: $recording") - isRemotelyRecorded.value = recording - } - - override fun onSnapshotTaken(call: Call, filePath: String) { - Log.i("[Call] Snapshot taken: $filePath") - val content = Factory.instance().createContent() - content.filePath = filePath - content.type = "image" - content.subtype = "jpeg" - content.name = filePath.substring(filePath.lastIndexOf("/") + 1) - - scope.launch { - if (Compatibility.addImageToMediaStore(coreContext.context, content)) { - Log.i("[Call] Added snapshot ${content.name} to Media Store") - val message = String.format( - AppUtils.getString(R.string.call_screenshot_taken), - content.name - ) - Toast.makeText(coreContext.context, message, Toast.LENGTH_SHORT).show() - } else { - Log.e("[Call] Something went wrong while copying file to Media Store...") - } - } - } - } - - private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - - init { - call.addListener(listener) - isRemotelyRecorded.value = call.remoteParams?.isRecording - displayableAddress.value = LinphoneUtils.getDisplayableAddress(call.remoteAddress) - - isConferenceCall.addSource(remoteConferenceSubject) { - isConferenceCall.value = remoteConferenceSubject.value.orEmpty().isNotEmpty() || conferenceParticipants.value.orEmpty().isNotEmpty() - } - isConferenceCall.addSource(conferenceParticipants) { - isConferenceCall.value = remoteConferenceSubject.value.orEmpty().isNotEmpty() || conferenceParticipants.value.orEmpty().isNotEmpty() - } - - update() - } - - override fun destroy() { - call.removeListener(listener) - timer?.cancel() - scope.cancel() - - super.destroy() - } - - fun togglePause() { - if (isCallPaused()) { - resume() - } else { - pause() - } - } - - fun pause() { - call.pause() - } - - fun resume() { - call.resume() - } - - fun accept() { - call.accept() - } - - fun terminate() { - call.terminate() - } - - fun toggleRecording() { - if (call.params.isRecording) { - call.stopRecording() - } else { - call.startRecording() - } - isRecording.value = call.params.isRecording - } - - fun showContextMenu(anchor: View) { - contextMenuClickListener?.onShowContextMenu(anchor, this) - } - - fun isActiveAndNotInConference(): Boolean { - return isPaused.value == false && isRemotelyPaused.value == false && call.conference?.call != null && isInRemoteConference.value == false - } - - private fun isCallPaused(): Boolean { - return when (call.state) { - Call.State.Paused, Call.State.Pausing -> true - else -> false - } - } - - private fun isCallRemotelyPaused(): Boolean { - return when (call.state) { - Call.State.PausedByRemote -> { - val conference = call.conference - if (conference != null && conference.me.isFocus) { - Log.w( - "[Call] State is paused by remote but we are the focus of the conference, so considering call as active" - ) - false - } else { - true - } - } - else -> false - } - } - - private fun canCallBePaused(): Boolean { - return !call.mediaInProgress() && when (call.state) { - Call.State.StreamsRunning, Call.State.PausedByRemote -> true - else -> false - } - } - - private fun update() { - isRecording.value = call.params.isRecording - isPaused.value = isCallPaused() - isRemotelyPaused.value = isCallRemotelyPaused() - canBePaused.value = canCallBePaused() - - updateConferenceInfo() - - isOutgoing.value = when (call.state) { - Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia, Call.State.OutgoingProgress, Call.State.OutgoingRinging -> true - else -> false - } - isIncoming.value = when (call.state) { - Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> true - else -> false - } - - // Check periodically until mediaInProgress is false - if (call.mediaInProgress()) { - scope.launch { - delay(1000) - update() - } - } - } - - private fun updateConferenceInfo() { - val conference = call.conference - isInRemoteConference.value = conference != null && conference.call != null - if (conference != null) { - Log.d("[Call] Found conference attached to call") - remoteConferenceSubject.value = LinphoneUtils.getConferenceSubject(conference) - Log.d( - "[Call] Found conference related to this call with subject [${remoteConferenceSubject.value}]" - ) - - val participantsList = arrayListOf() - for (participant in conference.participantList) { - val participantData = ConferenceInfoParticipantData(participant.address) - participantsList.add(participantData) - } - - conferenceParticipants.value = participantsList - conferenceParticipantsCountLabel.value = coreContext.context.getString( - R.string.conference_participants_title, - participantsList.size - ) - } else { - val conferenceAddress = LinphoneUtils.getConferenceAddress(call) - val conferenceInfo = if (conferenceAddress != null) { - coreContext.core.findConferenceInformationFromUri( - conferenceAddress - ) - } else { - null - } - if (conferenceInfo != null) { - Log.d( - "[Call] Found matching conference info with subject: ${conferenceInfo.subject}" - ) - remoteConferenceSubject.value = conferenceInfo.subject - - val participantsList = arrayListOf() - for (participant in conferenceInfo.participants) { - val participantData = ConferenceInfoParticipantData(participant) - participantsList.add(participantData) - } - - // Add organizer if not in participants list - val organizer = conferenceInfo.organizer - if (organizer != null) { - val found = participantsList.find { it.participant.weakEqual(organizer) } - if (found == null) { - val participantData = ConferenceInfoParticipantData(organizer) - participantsList.add(0, participantData) - } - } - - conferenceParticipants.value = participantsList - conferenceParticipantsCountLabel.value = coreContext.context.getString( - R.string.conference_participants_title, - participantsList.size - ) - } - } - } - - private fun startVideoUpdateAcceptanceTimer() { - timer?.cancel() - - timer = Timer("Call update timeout") - timer?.schedule( - object : TimerTask() { - override fun run() { - // Decline call update - coreContext.videoUpdateRequestTimedOut(call) - } - }, - 30000 - ) - Log.i("[Call] Starting 30 seconds timer to automatically decline video request") - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/data/CallStatisticsData.kt b/app/src/main/java/org/linphone/activities/voip/data/CallStatisticsData.kt deleted file mode 100644 index 6d4051df7..000000000 --- a/app/src/main/java/org/linphone/activities/voip/data/CallStatisticsData.kt +++ /dev/null @@ -1,148 +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 . - */ -package org.linphone.activities.voip.data - -import androidx.lifecycle.MutableLiveData -import org.linphone.contact.GenericContactData -import org.linphone.core.* - -class CallStatisticsData(val call: Call) : GenericContactData(call.remoteAddress) { - val audioStats = MutableLiveData>() - - val videoStats = MutableLiveData>() - - val mediaEncryptionStats = MutableLiveData>() - - val isVideoEnabled = MutableLiveData() - - private var enabled = false - - private val listener = object : CallListenerStub() { - override fun onStatsUpdated(call: Call, stats: CallStats) { - isVideoEnabled.value = call.currentParams.isVideoEnabled - updateCallStats(stats) - } - } - - init { - enabled = false - audioStats.value = arrayListOf() - videoStats.value = arrayListOf() - - initCallStats() - - val videoEnabled = call.currentParams.isVideoEnabled - isVideoEnabled.value = videoEnabled - - updateMediaEncryptionStats() - } - - fun enable() { - enabled = true - call.addListener(listener) - - // Needed for media encryption stats - updateMediaEncryptionStats() - } - - fun disable() { - enabled = false - call.removeListener(listener) - } - - override fun destroy() { - if (enabled) disable() - super.destroy() - } - - private fun updateMediaEncryptionStats() { - initCallStats() - } - - private fun initCallStats() { - val audioList = arrayListOf() - - audioList.add(StatItemData(StatType.CAPTURE)) - audioList.add(StatItemData(StatType.PLAYBACK)) - audioList.add(StatItemData(StatType.PAYLOAD)) - audioList.add(StatItemData(StatType.ENCODER)) - audioList.add(StatItemData(StatType.DECODER)) - audioList.add(StatItemData(StatType.DOWNLOAD_BW)) - audioList.add(StatItemData(StatType.UPLOAD_BW)) - audioList.add(StatItemData(StatType.ICE)) - audioList.add(StatItemData(StatType.IP_FAM)) - audioList.add(StatItemData(StatType.SENDER_LOSS)) - audioList.add(StatItemData(StatType.RECEIVER_LOSS)) - audioList.add(StatItemData(StatType.JITTER)) - - audioStats.value = audioList - - val mediaEncryptionList = arrayListOf() - - mediaEncryptionList.add(StatItemData(StatType.MEDIA_ENCRYPTION)) - - // ZRTP stats are only available when authentication token isn't null ! - if (call.currentParams.mediaEncryption == MediaEncryption.ZRTP && call.authenticationToken != null) { - mediaEncryptionList.add(StatItemData(StatType.ZRTP_CIPHER_ALGO)) - mediaEncryptionList.add(StatItemData(StatType.ZRTP_KEY_AGREEMENT_ALGO)) - mediaEncryptionList.add(StatItemData(StatType.ZRTP_HASH_ALGO)) - mediaEncryptionList.add(StatItemData(StatType.ZRTP_AUTH_TAG_ALGO)) - mediaEncryptionList.add(StatItemData(StatType.ZRTP_AUTH_SAS_ALGO)) - } - - mediaEncryptionStats.value = mediaEncryptionList - - val videoList = arrayListOf() - - videoList.add(StatItemData(StatType.CAPTURE)) - videoList.add(StatItemData(StatType.PLAYBACK)) - videoList.add(StatItemData(StatType.PAYLOAD)) - videoList.add(StatItemData(StatType.ENCODER)) - videoList.add(StatItemData(StatType.DECODER)) - videoList.add(StatItemData(StatType.DOWNLOAD_BW)) - videoList.add(StatItemData(StatType.UPLOAD_BW)) - videoList.add(StatItemData(StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW)) - videoList.add(StatItemData(StatType.ICE)) - videoList.add(StatItemData(StatType.IP_FAM)) - videoList.add(StatItemData(StatType.SENDER_LOSS)) - videoList.add(StatItemData(StatType.RECEIVER_LOSS)) - videoList.add(StatItemData(StatType.SENT_RESOLUTION)) - videoList.add(StatItemData(StatType.RECEIVED_RESOLUTION)) - videoList.add(StatItemData(StatType.SENT_FPS)) - videoList.add(StatItemData(StatType.RECEIVED_FPS)) - - videoStats.value = videoList - } - - private fun updateCallStats(stats: CallStats) { - if (stats.type == StreamType.Audio) { - for (stat in audioStats.value.orEmpty()) { - stat.update(call, stats) - } - for (stat in mediaEncryptionStats.value.orEmpty()) { - stat.update(call, stats) - } - } else if (stats.type == StreamType.Video) { - for (stat in videoStats.value.orEmpty()) { - stat.update(call, stats) - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/data/ConferenceInfoParticipantData.kt b/app/src/main/java/org/linphone/activities/voip/data/ConferenceInfoParticipantData.kt deleted file mode 100644 index bd7fe2eb6..000000000 --- a/app/src/main/java/org/linphone/activities/voip/data/ConferenceInfoParticipantData.kt +++ /dev/null @@ -1,31 +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 . - */ -package org.linphone.activities.voip.data - -import org.linphone.contact.GenericContactData -import org.linphone.core.* -import org.linphone.utils.LinphoneUtils - -class ConferenceInfoParticipantData( - val participant: Address -) : - GenericContactData(participant) { - val sipUri: String get() = LinphoneUtils.getDisplayableAddress(participant) -} diff --git a/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt b/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt deleted file mode 100644 index d0e679273..000000000 --- a/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantData.kt +++ /dev/null @@ -1,87 +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 . - */ -package org.linphone.activities.voip.data - -import androidx.lifecycle.MutableLiveData -import org.linphone.contact.GenericContactData -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.LinphoneUtils - -class ConferenceParticipantData( - val conference: Conference, - val participant: Participant -) : - GenericContactData(participant.address) { - val sipUri: String get() = LinphoneUtils.getDisplayableAddress(participant.address) - - val isAdmin = MutableLiveData() - val isMeAdmin = MutableLiveData() - val isSpeaker = MutableLiveData() - - init { - isAdmin.value = participant.isAdmin - isMeAdmin.value = conference.me.isAdmin - isSpeaker.value = participant.role == Participant.Role.Speaker - - Log.i( - "[Conference Participant] Participant ${participant.address.asStringUriOnly()} is ${if (participant.isAdmin) "admin" else "not admin"}" - ) - } - - fun setAdmin() { - if (conference.me.isAdmin) { - Log.i( - "[Conference Participant] Participant ${participant.address.asStringUriOnly()} will be set as admin" - ) - conference.setParticipantAdminStatus(participant, true) - } else { - Log.e( - "[Conference Participant] You aren't admin, you can't change participants admin rights" - ) - } - } - - fun unsetAdmin() { - if (conference.me.isAdmin) { - Log.i( - "[Conference Participant] Participant ${participant.address.asStringUriOnly()} will be unset as admin" - ) - conference.setParticipantAdminStatus(participant, false) - } else { - Log.e( - "[Conference Participant] You aren't admin, you can't change participants admin rights" - ) - } - } - - fun removeParticipantFromConference() { - if (conference.me.isAdmin) { - Log.i( - "[Conference Participant] Removing participant ${participant.address.asStringUriOnly()} from conference" - ) - conference.removeParticipant(participant) - } else { - Log.e( - "[Conference Participant] Can't remove participant ${participant.address.asStringUriOnly()} from conference, you aren't admin" - ) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantDeviceData.kt b/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantDeviceData.kt deleted file mode 100644 index 78cf5e860..000000000 --- a/app/src/main/java/org/linphone/activities/voip/data/ConferenceParticipantDeviceData.kt +++ /dev/null @@ -1,186 +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 . - */ -package org.linphone.activities.voip.data - -import android.view.TextureView -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.contact.GenericContactData -import org.linphone.core.* -import org.linphone.core.tools.Log - -class ConferenceParticipantDeviceData( - val participantDevice: ParticipantDevice, - val isMe: Boolean -) : - GenericContactData(participantDevice.address) { - val videoEnabled: MediatorLiveData = MediatorLiveData() - - val videoAvailable = MutableLiveData() - - val isSendingVideo = MutableLiveData() - - val isSpeaking = MutableLiveData() - - val isMuted = MutableLiveData() - - val isInConference = MutableLiveData() - - val isJoining = MutableLiveData() - - val isActiveSpeaker = MutableLiveData() - - private var textureView: TextureView? = null - - private val listener = object : ParticipantDeviceListenerStub() { - override fun onIsSpeakingChanged( - participantDevice: ParticipantDevice, - isSpeaking: Boolean - ) { - Log.i( - "[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] is ${if (isSpeaking) "speaking" else "not speaking"}" - ) - this@ConferenceParticipantDeviceData.isSpeaking.value = isSpeaking - } - - override fun onIsMuted(participantDevice: ParticipantDevice, isMuted: Boolean) { - Log.i( - "[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] is ${if (isMuted) "muted" else "not muted"}" - ) - this@ConferenceParticipantDeviceData.isMuted.value = isMuted - } - - override fun onStateChanged( - participantDevice: ParticipantDevice, - state: ParticipantDevice.State - ) { - Log.i( - "[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] state has changed: $state" - ) - when (state) { - ParticipantDevice.State.Joining, ParticipantDevice.State.Alerting -> isJoining.value = true - ParticipantDevice.State.OnHold -> { - isInConference.value = false - } - ParticipantDevice.State.Present -> { - isJoining.value = false - isInConference.value = true - updateWindowId(textureView) - } - else -> {} - } - } - - override fun onStreamCapabilityChanged( - participantDevice: ParticipantDevice, - direction: MediaDirection, - streamType: StreamType - ) { - if (streamType == StreamType.Video) { - Log.i( - "[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] video capability changed to $direction" - ) - isSendingVideo.value = direction == MediaDirection.SendRecv || direction == MediaDirection.SendOnly - } - } - - override fun onStreamAvailabilityChanged( - participantDevice: ParticipantDevice, - available: Boolean, - streamType: StreamType - ) { - if (streamType == StreamType.Video) { - Log.i( - "[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}] video availability changed to ${if (available) "available" else "unavailable"}" - ) - videoAvailable.value = available - if (available) { - updateWindowId(textureView) - } - } - } - } - - init { - Log.i( - "[Conference Participant Device] Created device width Address [${participantDevice.address.asStringUriOnly()}], is it myself? $isMe" - ) - participantDevice.addListener(listener) - - isSpeaking.value = false - isActiveSpeaker.value = false - isMuted.value = participantDevice.isMuted - - videoAvailable.value = participantDevice.getStreamAvailability(StreamType.Video) - val videoCapability = participantDevice.getStreamCapability(StreamType.Video) - isSendingVideo.value = videoCapability == MediaDirection.SendRecv || videoCapability == MediaDirection.SendOnly - isInConference.value = participantDevice.isInConference - - val state = participantDevice.state - isJoining.value = state == ParticipantDevice.State.Joining || state == ParticipantDevice.State.Alerting - Log.i( - "[Conference Participant Device] State for participant [${participantDevice.address.asStringUriOnly()}] is $state" - ) - - videoEnabled.value = isVideoAvailableAndSendReceive() - videoEnabled.addSource(videoAvailable) { - videoEnabled.value = isVideoAvailableAndSendReceive() - } - videoEnabled.addSource(isSendingVideo) { - videoEnabled.value = isVideoAvailableAndSendReceive() - } - - Log.i( - "[Conference Participant Device] Participant [${participantDevice.address.asStringUriOnly()}], is in conf? ${isInConference.value}, is video available? ${videoAvailable.value} ($videoCapability), is mic muted? ${isMuted.value}" - ) - } - - override fun destroy() { - participantDevice.removeListener(listener) - - super.destroy() - } - - fun switchCamera() { - coreContext.switchCamera() - } - - fun isSwitchCameraAvailable(): Boolean { - return isMe && coreContext.showSwitchCameraButton() - } - - fun setTextureView(tv: TextureView) { - textureView = tv - - Log.i( - "[Conference Participant Device] Setting textureView [$textureView] for participant [${participantDevice.address.asStringUriOnly()}]" - ) - updateWindowId(textureView) - } - - private fun updateWindowId(windowId: Any?) { - participantDevice.nativeVideoWindowId = windowId - } - - private fun isVideoAvailableAndSendReceive(): Boolean { - return videoAvailable.value == true && isSendingVideo.value == true - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/data/StatItemData.kt b/app/src/main/java/org/linphone/activities/voip/data/StatItemData.kt deleted file mode 100644 index b69fd4edc..000000000 --- a/app/src/main/java/org/linphone/activities/voip/data/StatItemData.kt +++ /dev/null @@ -1,127 +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 . - */ -package org.linphone.activities.voip.data - -import androidx.lifecycle.MutableLiveData -import java.text.DecimalFormat -import org.linphone.R -import org.linphone.core.* -import org.linphone.utils.AppUtils - -enum class StatType(val nameResource: Int) { - CAPTURE(R.string.call_stats_capture_filter), - PLAYBACK(R.string.call_stats_player_filter), - PAYLOAD(R.string.call_stats_codec), - ENCODER(R.string.call_stats_encoder_name), - DECODER(R.string.call_stats_decoder_name), - DOWNLOAD_BW(R.string.call_stats_download), - UPLOAD_BW(R.string.call_stats_upload), - ICE(R.string.call_stats_ice), - IP_FAM(R.string.call_stats_ip), - SENDER_LOSS(R.string.call_stats_sender_loss_rate), - RECEIVER_LOSS(R.string.call_stats_receiver_loss_rate), - JITTER(R.string.call_stats_jitter_buffer), - SENT_RESOLUTION(R.string.call_stats_video_resolution_sent), - RECEIVED_RESOLUTION(R.string.call_stats_video_resolution_received), - SENT_FPS(R.string.call_stats_video_fps_sent), - RECEIVED_FPS(R.string.call_stats_video_fps_received), - ESTIMATED_AVAILABLE_DOWNLOAD_BW(R.string.call_stats_estimated_download), - MEDIA_ENCRYPTION(R.string.call_stats_media_encryption), - ZRTP_CIPHER_ALGO(R.string.call_stats_zrtp_cipher_algo), - ZRTP_KEY_AGREEMENT_ALGO(R.string.call_stats_zrtp_key_agreement_algo), - ZRTP_HASH_ALGO(R.string.call_stats_zrtp_hash_algo), - ZRTP_AUTH_TAG_ALGO(R.string.call_stats_zrtp_auth_tag_algo), - ZRTP_AUTH_SAS_ALGO(R.string.call_stats_zrtp_sas_algo) -} - -class StatItemData(val type: StatType) { - companion object { - fun audioDeviceToString(device: AudioDevice?): String { - if (device == null) return "null" - return "${device.deviceName} [${device.type}] (${device.driverName})" - } - } - - val value = MutableLiveData() - - fun update(call: Call, stats: CallStats) { - val payloadType = if (stats.type == StreamType.Audio) call.currentParams.usedAudioPayloadType else call.currentParams.usedVideoPayloadType - payloadType ?: return - value.value = when (type) { - StatType.CAPTURE -> if (stats.type == StreamType.Audio) { - audioDeviceToString( - call.inputAudioDevice - ) - } else { - call.core.videoDevice - } - StatType.PLAYBACK -> if (stats.type == StreamType.Audio) { - audioDeviceToString( - call.outputAudioDevice - ) - } else { - call.core.videoDisplayFilter - } - StatType.PAYLOAD -> "${payloadType.mimeType}/${payloadType.clockRate / 1000} kHz" - StatType.ENCODER -> call.core.mediastreamerFactory.getDecoderText(payloadType.mimeType) - StatType.DECODER -> call.core.mediastreamerFactory.getEncoderText(payloadType.mimeType) - StatType.DOWNLOAD_BW -> "${stats.downloadBandwidth} kbits/s" - StatType.UPLOAD_BW -> "${stats.uploadBandwidth} kbits/s" - StatType.ICE -> stats.iceState.toString() - StatType.IP_FAM -> if (stats.ipFamilyOfRemote == Address.Family.Inet6) "IPv6" else "IPv4" - StatType.SENDER_LOSS -> DecimalFormat("##.##%").format(stats.senderLossRate) - StatType.RECEIVER_LOSS -> DecimalFormat("##.##%").format(stats.receiverLossRate) - StatType.JITTER -> DecimalFormat("##.## ms").format(stats.jitterBufferSizeMs) - StatType.SENT_RESOLUTION -> call.currentParams.sentVideoDefinition?.name - StatType.RECEIVED_RESOLUTION -> call.currentParams.receivedVideoDefinition?.name - StatType.SENT_FPS -> "${call.currentParams.sentFramerate}" - StatType.RECEIVED_FPS -> "${call.currentParams.receivedFramerate}" - StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW -> "${stats.estimatedDownloadBandwidth} kbit/s" - StatType.MEDIA_ENCRYPTION -> { - when (call.currentParams.mediaEncryption) { - MediaEncryption.ZRTP -> { - if (stats.isZrtpKeyAgreementAlgoPostQuantum) { - AppUtils.getString( - R.string.call_settings_media_encryption_zrtp_post_quantum - ) - } else { - AppUtils.getString(R.string.call_settings_media_encryption_zrtp) - } - } - MediaEncryption.DTLS -> AppUtils.getString( - R.string.call_settings_media_encryption_dtls - ) - MediaEncryption.SRTP -> AppUtils.getString( - R.string.call_settings_media_encryption_srtp - ) - MediaEncryption.None -> AppUtils.getString( - R.string.call_settings_media_encryption_none - ) - else -> "Unexpected!" - } - } - StatType.ZRTP_CIPHER_ALGO -> stats.zrtpCipherAlgo - StatType.ZRTP_KEY_AGREEMENT_ALGO -> stats.zrtpKeyAgreementAlgo - StatType.ZRTP_HASH_ALGO -> stats.zrtpHashAlgo - StatType.ZRTP_AUTH_TAG_ALGO -> stats.zrtpAuthTagAlgo - StatType.ZRTP_AUTH_SAS_ALGO -> stats.zrtpSasAlgo - } - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt deleted file mode 100644 index d48bf8e71..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/CallsListFragment.kt +++ /dev/null @@ -1,189 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.content.Intent -import android.os.Bundle -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.widget.PopupWindow -import androidx.databinding.DataBindingUtil -import androidx.navigation.navGraphViewModels -import org.linphone.R -import org.linphone.activities.main.MainActivity -import org.linphone.activities.voip.ConferenceDisplayMode -import org.linphone.activities.voip.data.CallData -import org.linphone.activities.voip.viewmodels.CallsViewModel -import org.linphone.activities.voip.viewmodels.ConferenceViewModel -import org.linphone.activities.voip.viewmodels.ControlsViewModel -import org.linphone.databinding.VoipCallContextMenuBindingImpl -import org.linphone.databinding.VoipCallsListFragmentBinding -import org.linphone.utils.AppUtils - -class CallsListFragment : GenericVideoPreviewFragment() { - private val callsViewModel: CallsViewModel by navGraphViewModels(R.id.call_nav_graph) - private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph) - private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph) - - override fun getLayoutId(): Int = R.layout.voip_calls_list_fragment - - private val callContextMenuClickListener = object : CallData.CallContextMenuClickListener { - override fun onShowContextMenu(anchor: View, callData: CallData) { - showCallMenu(anchor, callData) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.callsViewModel = callsViewModel - - binding.conferenceViewModel = conferenceViewModel - - binding.controlsViewModel = controlsViewModel - - binding.setCancelClickListener { - goBack() - } - - binding.setAddCallClickListener { - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - intent.putExtra("Dialer", true) - intent.putExtra("Transfer", false) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } - - callsViewModel.callsData.observe( - viewLifecycleOwner - ) { - for (data in it) { - data.contextMenuClickListener = callContextMenuClickListener - } - } - - conferenceViewModel.conferenceDisplayMode.observe( - viewLifecycleOwner - ) { - binding.localPreviewVideoSurface.visibility = if (it == ConferenceDisplayMode.AUDIO_ONLY) { - View.GONE - } else { - View.VISIBLE - } - } - } - - override fun onResume() { - super.onResume() - - setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera) - } - - override fun onPause() { - super.onPause() - - cleanUpLocalVideoPreview(binding.localPreviewVideoSurface) - } - - private fun showCallMenu(anchor: View, callData: CallData) { - val popupView: VoipCallContextMenuBindingImpl = DataBindingUtil.inflate( - LayoutInflater.from(requireContext()), - R.layout.voip_call_context_menu, - null, - false - ) - - val itemSize = AppUtils.getDimension(R.dimen.voip_call_context_menu_item_height).toInt() - var totalSize = itemSize * 5 - - if (callData.isPaused.value == true || - callData.isIncoming.value == true || - callData.isOutgoing.value == true || - callData.isInRemoteConference.value == true - ) { - popupView.hidePause = true - totalSize -= itemSize - } - - if (callData.isIncoming.value == true || - callData.isOutgoing.value == true || - callData.isInRemoteConference.value == true - ) { - popupView.hideResume = true - popupView.hideTransfer = true - totalSize -= itemSize * 2 - } else if (callData.isPaused.value == false) { - popupView.hideResume = true - totalSize -= itemSize - } - - if (callData.isIncoming.value == false) { - popupView.hideAccept = true - totalSize -= itemSize - } - - // 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.voip_call_context_menu_width).toInt(), - totalSize, - true - ) - // Elevation is for showing a shadow around the popup - popupWindow.elevation = 20f - - popupView.setResumeClickListener { - callData.resume() - popupWindow.dismiss() - } - - popupView.setPauseClickListener { - callData.pause() - popupWindow.dismiss() - } - - popupView.setTransferClickListener { - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - intent.putExtra("Dialer", true) - intent.putExtra("Transfer", true) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - popupWindow.dismiss() - } - - popupView.setAnswerClickListener { - callData.accept() - popupWindow.dismiss() - } - - popupView.setHangupClickListener { - callData.terminate() - popupWindow.dismiss() - } - - popupWindow.showAsDropDown(anchor, 0, 0, Gravity.END or Gravity.TOP) - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ChatFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ChatFragment.kt deleted file mode 100644 index ab9bdf69b..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/ChatFragment.kt +++ /dev/null @@ -1,267 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.app.Activity -import android.content.Intent -import android.content.pm.PackageManager -import android.os.Bundle -import android.os.Parcelable -import android.provider.MediaStore -import android.view.View -import androidx.core.content.FileProvider -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.launch -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter -import org.linphone.activities.main.chat.data.ChatMessageData -import org.linphone.activities.main.chat.viewmodels.* -import org.linphone.activities.main.viewmodels.ListTopBarViewModel -import org.linphone.compatibility.Compatibility -import org.linphone.core.ChatRoom -import org.linphone.core.Factory -import org.linphone.core.tools.Log -import org.linphone.databinding.VoipChatFragmentBinding -import org.linphone.utils.FileUtils -import org.linphone.utils.PermissionHelper - -class ChatFragment : GenericFragment() { - private lateinit var adapter: ChatMessagesListAdapter - private lateinit var viewModel: ChatRoomViewModel - private lateinit var listViewModel: ChatMessagesListViewModel - private lateinit var chatSendingViewModel: ChatMessageSendingViewModel - - override fun getLayoutId(): Int = R.layout.voip_chat_fragment - - 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() - } - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.setCancelClickListener { - goBack() - } - - binding.setChatRoomsListClickListener { - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - intent.putExtra("Chat", true) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } - - binding.setAttachFileClickListener { - if (PermissionHelper.get().hasReadExternalStoragePermission() || PermissionHelper.get().hasCameraPermission()) { - pickFile() - } else { - Log.i("[Chat] Asking for READ_EXTERNAL_STORAGE and CAMERA permissions") - Compatibility.requestReadExternalStorageAndCameraPermissions(this, 0) - } - } - - val localSipUri = arguments?.getString("LocalSipUri") - val remoteSipUri = arguments?.getString("RemoteSipUri") - var chatRoom: ChatRoom? = null - if (localSipUri != null && remoteSipUri != null) { - Log.i( - "[Chat] 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) - ) - } - chatRoom ?: return - - viewModel = requireActivity().run { - 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() - - val layoutManager = LinearLayoutManager(requireContext()) - layoutManager.stackFromEnd = true - binding.chatMessagesList.layoutManager = layoutManager - - listViewModel.events.observe( - viewLifecycleOwner - ) { events -> - adapter.submitList(events) - } - - chatSendingViewModel.textToSend.observe( - viewLifecycleOwner - ) { - chatSendingViewModel.onTextToSendChanged(it) - } - - adapter.replyMessageEvent.observe( - viewLifecycleOwner - ) { - it.consume { chatMessage -> - chatSendingViewModel.pendingChatMessageToReplyTo.value?.destroy() - chatSendingViewModel.pendingChatMessageToReplyTo.value = - ChatMessageData(chatMessage) - chatSendingViewModel.isPendingAnswer.value = true - } - } - } - - override fun onResume() { - super.onResume() - - if (this::viewModel.isInitialized) { - // Prevent notifications for this chat room to be displayed - val peerAddress = viewModel.chatRoom.peerAddress.asStringUriOnly() - coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = peerAddress - viewModel.chatRoom.markAsRead() - } else { - Log.e("[Chat] Fragment resuming but viewModel lateinit property isn't initialized!") - } - } - - override fun onPause() { - coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null - - super.onPause() - } - - @Deprecated("Deprecated in Java") - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (resultCode == Activity.RESULT_OK) { - lifecycleScope.launch { - for ( - fileToUploadPath in FileUtils.getFilesPathFromPickerIntent( - data, - chatSendingViewModel.temporaryFileUploadPath - ) - ) { - chatSendingViewModel.addAttachment(fileToUploadPath) - } - } - } - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - var atLeastOneGranted = false - for (result in grantResults) { - atLeastOneGranted = atLeastOneGranted || result == PackageManager.PERMISSION_GRANTED - } - - when (requestCode) { - 0 -> { - if (atLeastOneGranted) { - pickFile() - } - } - } - } - - private fun scrollToBottom() { - if (adapter.itemCount > 0) { - binding.chatMessagesList.scrollToPosition(adapter.itemCount - 1) - } - } - - private fun pickFile() { - val intentsList = ArrayList() - - val pickerIntent = Intent(Intent.ACTION_GET_CONTENT) - pickerIntent.type = "*/*" - pickerIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) - - if (PermissionHelper.get().hasCameraPermission()) { - // Allows to capture directly from the camera - val capturePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - val tempFileName = System.currentTimeMillis().toString() + ".jpeg" - val file = FileUtils.getFileStoragePath(tempFileName) - chatSendingViewModel.temporaryFileUploadPath = file - val publicUri = FileProvider.getUriForFile( - requireContext(), - requireContext().getString(R.string.file_provider), - file - ) - capturePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, publicUri) - capturePictureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - capturePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - intentsList.add(capturePictureIntent) - - val captureVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) - intentsList.add(captureVideoIntent) - } - - val chooserIntent = - Intent.createChooser(pickerIntent, getString(R.string.chat_message_pick_file_dialog)) - chooserIntent.putExtra( - Intent.EXTRA_INITIAL_INTENTS, - intentsList.toArray(arrayOf()) - ) - - startActivityForResult(chooserIntent, 0) - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceAddParticipantsFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceAddParticipantsFragment.kt deleted file mode 100644 index 4b588a2eb..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceAddParticipantsFragment.kt +++ /dev/null @@ -1,134 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.navGraphViewModels -import androidx.recyclerview.widget.LinearLayoutManager -import org.linphone.LinphoneApplication -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.voip.viewmodels.ConferenceParticipantsViewModel -import org.linphone.activities.voip.viewmodels.ConferenceParticipantsViewModelFactory -import org.linphone.activities.voip.viewmodels.ConferenceViewModel -import org.linphone.contact.ContactsSelectionAdapter -import org.linphone.core.tools.Log -import org.linphone.databinding.VoipConferenceParticipantsAddFragmentBinding -import org.linphone.utils.AppUtils -import org.linphone.utils.PermissionHelper - -class ConferenceAddParticipantsFragment : GenericFragment() { - private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph) - private lateinit var viewModel: ConferenceParticipantsViewModel - private lateinit var adapter: ContactsSelectionAdapter - - override fun getLayoutId(): Int = R.layout.voip_conference_participants_add_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - val conference = conferenceViewModel.conference.value - conference ?: return - - viewModel = ViewModelProvider( - this, - ConferenceParticipantsViewModelFactory(conference) - )[ConferenceParticipantsViewModel::class.java] - - binding.viewModel = viewModel - - adapter = ContactsSelectionAdapter(viewLifecycleOwner) - adapter.setLimeCapabilityRequired(false) // TODO: Use right value from conference - binding.contactsList.adapter = adapter - - val layoutManager = LinearLayoutManager(requireContext()) - binding.contactsList.layoutManager = layoutManager - - // Divider between items - binding.contactsList.addItemDecoration( - AppUtils.getDividerDecoration(requireContext(), layoutManager) - ) - - binding.setApplyClickListener { - viewModel.applyChanges() - goBack() - } - - viewModel.contactsList.observe( - viewLifecycleOwner - ) { - adapter.submitList(it) - } - viewModel.sipContactsSelected.observe( - viewLifecycleOwner - ) { - viewModel.applyFilter() - } - - viewModel.selectedAddresses.observe( - viewLifecycleOwner - ) { - adapter.updateSelectedAddresses(it) - } - viewModel.filter.observe( - viewLifecycleOwner - ) { - viewModel.applyFilter() - } - - adapter.selectedContact.observe( - viewLifecycleOwner - ) { - it.consume { searchResult -> - viewModel.toggleSelectionForSearchResult(searchResult) - } - } - - if (corePreferences.enableNativeAddressBookIntegration) { - if (!PermissionHelper.get().hasReadContactsPermission()) { - Log.i("[Conference Add Participants] Asking for READ_CONTACTS permission") - requestPermissions(arrayOf(android.Manifest.permission.READ_CONTACTS), 0) - } - } - } - - @Deprecated("Deprecated in Java") - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == 0) { - val granted = grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED - if (granted) { - Log.i("[Conference Add Participants] READ_CONTACTS permission granted") - LinphoneApplication.coreContext.fetchContacts() - } else { - Log.w("[Conference Add Participants] READ_CONTACTS permission denied") - } - } - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceCallFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceCallFragment.kt deleted file mode 100644 index e8b922067..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceCallFragment.kt +++ /dev/null @@ -1,592 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.annotation.SuppressLint -import android.content.Intent -import android.content.res.Configuration -import android.os.Bundle -import android.os.SystemClock -import android.view.View -import android.view.animation.AccelerateDecelerateInterpolator -import android.widget.Chronometer -import android.widget.Toast -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import androidx.navigation.navGraphViewModels -import androidx.transition.AutoTransition -import androidx.transition.TransitionManager -import androidx.window.layout.FoldingFeature -import com.google.android.material.snackbar.Snackbar -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.* -import org.linphone.activities.main.MainActivity -import org.linphone.activities.voip.ConferenceDisplayMode -import org.linphone.activities.voip.viewmodels.CallsViewModel -import org.linphone.activities.voip.viewmodels.ConferenceViewModel -import org.linphone.activities.voip.viewmodels.ControlsViewModel -import org.linphone.activities.voip.viewmodels.StatisticsListViewModel -import org.linphone.activities.voip.views.RoundCornersTextureView -import org.linphone.core.Conference -import org.linphone.core.StreamType -import org.linphone.core.tools.Log -import org.linphone.databinding.VoipConferenceCallFragmentBinding - -class ConferenceCallFragment : GenericFragment() { - private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph) - private val callsViewModel: CallsViewModel by navGraphViewModels(R.id.call_nav_graph) - private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph) - private val statsViewModel: StatisticsListViewModel by navGraphViewModels(R.id.call_nav_graph) - - override fun getLayoutId(): Int = R.layout.voip_conference_call_fragment - - override fun onStart() { - useMaterialSharedAxisXForwardAnimation = false - - super.onStart() - } - - @SuppressLint("CutPasteId") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - controlsViewModel.hideCallStats() // In case it was toggled on during incoming/outgoing fragment was visible - - binding.lifecycleOwner = viewLifecycleOwner - - binding.controlsViewModel = controlsViewModel - - binding.callsViewModel = callsViewModel - - binding.conferenceViewModel = conferenceViewModel - - binding.statsViewModel = statsViewModel - - conferenceViewModel.reloadConferenceFragmentEvent.observe( - viewLifecycleOwner - ) { - it.consume { - Log.i( - "[Conference Call] Reloading fragment after toggling video ON while in AUDIO_ONLY layout" - ) - refreshConferenceFragment() - } - } - - conferenceViewModel.conferenceDisplayMode.observe( - viewLifecycleOwner - ) { displayMode -> - startTimer(R.id.active_conference_timer) - - if (displayMode == ConferenceDisplayMode.ACTIVE_SPEAKER) { - if (conferenceViewModel.conferenceExists.value == true) { - Log.i( - "[Conference Call] Local participant is in conference and current layout is active speaker, updating Core's native window id" - ) - val window = binding.root.findViewById( - R.id.conference_active_speaker_remote_video - ) - coreContext.core.nativeVideoWindowId = window - - val preview = binding.root.findViewById( - R.id.local_preview_video_surface - ) - if (preview != null) { - conferenceViewModel.meParticipant.value?.setTextureView(preview) - } - } else { - Log.i( - "[Conference Call] Either not in conference or current layout isn't active speaker, updating Core's native window id" - ) - coreContext.core.nativeVideoWindowId = null - } - } - - when (displayMode) { - ConferenceDisplayMode.AUDIO_ONLY -> { - controlsViewModel.fullScreenMode.value = false - } - else -> { - val conference = conferenceViewModel.conference.value - if (conference != null) switchToFullScreenIfPossible(conference) - } - } - } - - conferenceViewModel.conferenceParticipantDevices.observe( - viewLifecycleOwner - ) { - if ( - conferenceViewModel.conferenceDisplayMode.value == ConferenceDisplayMode.GRID && - it.size > conferenceViewModel.maxParticipantsForMosaicLayout - ) { - Log.w( - "[Conference Call] More than ${conferenceViewModel.maxParticipantsForMosaicLayout} participants (${it.size}), forcing active speaker layout" - ) - conferenceViewModel.changeLayout(ConferenceDisplayMode.ACTIVE_SPEAKER, false) - refreshConferenceFragment() - // Can't use SnackBar whilst changing fragment - Toast.makeText( - requireContext(), - R.string.conference_too_many_participants_for_mosaic_layout, - Toast.LENGTH_LONG - ).show() - } - } - - conferenceViewModel.secondParticipantJoinedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - switchToActiveSpeakerLayoutForTwoParticipants() - } - } - - conferenceViewModel.moreThanTwoParticipantsJoinedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - switchToActiveSpeakerLayoutForMoreThanTwoParticipants() - } - } - - conferenceViewModel.conference.observe( - viewLifecycleOwner - ) { conference -> - if (conference != null) switchToFullScreenIfPossible(conference) - } - - conferenceViewModel.conferenceCreationPending.observe( - viewLifecycleOwner - ) { creationPending -> - if (!creationPending) { - val conference = conferenceViewModel.conference.value - if (conference != null) switchToFullScreenIfPossible(conference) - } - } - - conferenceViewModel.firstToJoinEvent.observe( - viewLifecycleOwner - ) { - it.consume { - Snackbar - .make( - binding.coordinator, - R.string.conference_first_to_join, - Snackbar.LENGTH_LONG - ) - .setAnchorView(binding.primaryButtons.hangup) - .show() - } - } - - conferenceViewModel.allParticipantsLeftEvent.observe( - viewLifecycleOwner - ) { - it.consume { - Snackbar - .make(binding.coordinator, R.string.conference_last_user, Snackbar.LENGTH_LONG) - .setAnchorView(binding.primaryButtons.hangup) - .show() - - switchToActiveSpeakerLayoutWhenAlone() - } - } - - controlsViewModel.goToConferenceParticipantsListEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToConferenceParticipants() - } - } - - controlsViewModel.goToChatEvent.observe( - viewLifecycleOwner - ) { - it.consume { - goToChat() - } - } - - controlsViewModel.goToCallsListEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToCallsList() - } - } - - controlsViewModel.goToConferenceLayoutSettingsEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToConferenceLayout() - } - } - - controlsViewModel.foldingState.observe( - viewLifecycleOwner - ) { feature -> - updateHingeRelatedConstraints(feature) - } - - callsViewModel.callUpdateEvent.observe( - viewLifecycleOwner - ) { - it.consume { call -> - val conference = call.conference - if (conference != null && conferenceViewModel.conference.value == null) { - Log.i( - "[Conference Call] Found conference attached to call and no conference in dedicated view model, init & configure it" - ) - conferenceViewModel.initConference(conference) - conferenceViewModel.configureConference(conference) - } - } - } - - controlsViewModel.goToDialerEvent.observe( - viewLifecycleOwner - ) { - it.consume { isCallTransfer -> - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - if (corePreferences.skipDialerForNewCallAndTransfer) { - intent.putExtra("Contacts", true) - } else { - intent.putExtra("Dialer", true) - } - intent.putExtra("Transfer", isCallTransfer) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } - } - } - - override fun onPause() { - super.onPause() - - controlsViewModel.hideExtraButtons(true) - } - - override fun onResume() { - super.onResume() - - if (conferenceViewModel.conferenceDisplayMode.value == ConferenceDisplayMode.ACTIVE_SPEAKER) { - Log.i( - "[Conference Call] Conference fragment is resuming, current display mode is active speaker, adjusting layout" - ) - adjustActiveSpeakerLayout() - } - } - - private fun switchToFullScreenIfPossible(conference: Conference) { - if (corePreferences.enableFullScreenWhenJoiningVideoConference) { - if (conference.currentParams.isVideoEnabled) { - when { - conference.me.devices.isEmpty() -> { - Log.i( - "[Conference Call] Conference has video enabled but our device hasn't joined yet" - ) - } - conference.me.devices.find { - it.isInConference && it.getStreamAvailability( - StreamType.Video - ) - } != null -> { - Log.i( - "[Conference Call] Conference has video enabled & our device has video enabled, enabling full screen mode" - ) - controlsViewModel.fullScreenMode.value = true - } - else -> { - Log.i( - "[Conference Call] Conference has video enabled but our device video is disabled" - ) - } - } - } - } - } - - private fun goToChat() { - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - intent.putExtra("Chat", true) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } - - private fun startTimer(timerId: Int) { - val timer: Chronometer? = binding.root.findViewById(timerId) - if (timer == null) { - Log.w("[Conference Call] Timer not found, maybe view wasn't inflated yet?") - return - } - - val conference = conferenceViewModel.conference.value - if (conference != null) { - val duration = 1000 * conference.duration // Linphone timestamps are in seconds - timer.base = SystemClock.elapsedRealtime() - duration - } else { - Log.e("[Conference Call] Conference not found, timer will have no base") - } - - timer.start() - } - - private fun updateHingeRelatedConstraints(feature: FoldingFeature) { - Log.i("[Conference Call] Updating constraint layout hinges: $feature") - val constraintLayout = binding.root.findViewById( - R.id.conference_constraint_layout - ) - ?: return - val set = ConstraintSet() - set.clone(constraintLayout) - - // Only modify UI in table top mode - if (feature.orientation == FoldingFeature.Orientation.HORIZONTAL && - feature.state == FoldingFeature.State.HALF_OPENED - ) { - set.setGuidelinePercent(R.id.hinge_top, 0.5f) - set.setGuidelinePercent(R.id.hinge_bottom, 0.5f) - controlsViewModel.folded.value = true - } else { - set.setGuidelinePercent(R.id.hinge_top, 0f) - set.setGuidelinePercent(R.id.hinge_bottom, 1f) - controlsViewModel.folded.value = false - } - - set.applyTo(constraintLayout) - } - - private fun animateConstraintLayout( - constraintLayout: ConstraintLayout, - set: ConstraintSet - ) { - val trans = AutoTransition() - trans.duration = 500 - trans.interpolator = AccelerateDecelerateInterpolator() - TransitionManager.beginDelayedTransition(constraintLayout, trans) - set.applyTo(constraintLayout) - } - - private fun adjustActiveSpeakerLayout() { - if (conferenceViewModel.conference.value?.state == Conference.State.Created) { - val participantsCount = conferenceViewModel.conferenceParticipantDevices.value.orEmpty().size - Log.i( - "[Conference Call] Updating active speaker layout for [$participantsCount] participants" - ) - when (participantsCount) { - 1 -> switchToActiveSpeakerLayoutWhenAlone() - 2 -> switchToActiveSpeakerLayoutForTwoParticipants() - else -> switchToActiveSpeakerLayoutForMoreThanTwoParticipants() - } - } else { - Log.w( - "[Conference] Active speaker layout not adjusted, conference state is: ${conferenceViewModel.conference.value?.state}" - ) - } - } - - private fun getConstraintSet(constraintLayout: ConstraintLayout): ConstraintSet { - val set = ConstraintSet() - set.clone(constraintLayout) - - set.clear(R.id.local_participant_background, ConstraintSet.TOP) - set.clear(R.id.local_participant_background, ConstraintSet.START) - set.clear(R.id.local_participant_background, ConstraintSet.LEFT) - set.clear(R.id.local_participant_background, ConstraintSet.BOTTOM) - set.clear(R.id.local_participant_background, ConstraintSet.END) - set.clear(R.id.local_participant_background, ConstraintSet.RIGHT) - - return set - } - - private fun switchToActiveSpeakerLayoutForMoreThanTwoParticipants() { - if (conferenceViewModel.conferenceDisplayMode.value != ConferenceDisplayMode.ACTIVE_SPEAKER) return - - val constraintLayout = - binding.root.findViewById(R.id.conference_constraint_layout) - ?: return - val set = getConstraintSet(constraintLayout) - - val margin = resources.getDimension(R.dimen.voip_active_speaker_miniature_margin).toInt() - val portraitOrientation = resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE - if (portraitOrientation) { - set.connect( - R.id.local_participant_background, - ConstraintSet.START, - R.id.conference_constraint_layout, - ConstraintSet.START, - margin - ) - set.connect( - R.id.local_participant_background, - ConstraintSet.BOTTOM, - R.id.miniatures, - ConstraintSet.BOTTOM, - 0 - ) - set.connect( - R.id.local_participant_background, - ConstraintSet.TOP, - R.id.miniatures, - ConstraintSet.TOP, - 0 - ) - } else { - set.connect( - R.id.local_participant_background, - ConstraintSet.BOTTOM, - R.id.hinge_bottom, - ConstraintSet.BOTTOM, - 0 - ) - set.connect( - R.id.local_participant_background, - ConstraintSet.START, - R.id.active_speaker_background, - ConstraintSet.END, - 0 - ) - set.connect( - R.id.local_participant_background, - ConstraintSet.END, - R.id.scroll_indicator, - ConstraintSet.START, - 0 - ) - } - - val size = resources.getDimension(R.dimen.voip_active_speaker_miniature_size).toInt() - set.constrainWidth(R.id.local_participant_background, size) - set.constrainHeight(R.id.local_participant_background, size) - - val avatarSize = resources.getDimension( - R.dimen.voip_conference_active_speaker_miniature_avatar_size - ).toInt() - set.constrainWidth(R.id.local_participant_avatar, avatarSize) - set.constrainHeight(R.id.local_participant_avatar, avatarSize) - - Log.i("[Conference Call] Updating active speaker layout for 3 or more participants") - if (corePreferences.enableAnimations) { - animateConstraintLayout(constraintLayout, set) - } else { - set.applyTo(constraintLayout) - } - } - - private fun switchToActiveSpeakerLayoutForTwoParticipants() { - if (conferenceViewModel.conferenceDisplayMode.value != ConferenceDisplayMode.ACTIVE_SPEAKER) return - - val constraintLayout = - binding.root.findViewById(R.id.conference_constraint_layout) - ?: return - val set = getConstraintSet(constraintLayout) - - val margin = resources.getDimension(R.dimen.voip_active_speaker_miniature_margin).toInt() - set.connect( - R.id.local_participant_background, - ConstraintSet.BOTTOM, - R.id.hinge_bottom, - ConstraintSet.BOTTOM, - margin - ) - // Don't know why but if we use END instead of RIGHT, margin isn't applied... - set.connect( - R.id.local_participant_background, - ConstraintSet.RIGHT, - R.id.conference_constraint_layout, - ConstraintSet.RIGHT, - margin - ) - - val size = resources.getDimension(R.dimen.voip_active_speaker_miniature_size).toInt() - set.constrainWidth(R.id.local_participant_background, size) - set.constrainHeight(R.id.local_participant_background, size) - - val avatarSize = resources.getDimension( - R.dimen.voip_conference_active_speaker_miniature_avatar_size - ).toInt() - set.constrainWidth(R.id.local_participant_avatar, avatarSize) - set.constrainHeight(R.id.local_participant_avatar, avatarSize) - - Log.i("[Conference Call] Updating active speaker layout for 2 participants") - if (corePreferences.enableAnimations) { - animateConstraintLayout(constraintLayout, set) - } else { - set.applyTo(constraintLayout) - } - } - - private fun switchToActiveSpeakerLayoutWhenAlone() { - if (conferenceViewModel.conferenceDisplayMode.value != ConferenceDisplayMode.ACTIVE_SPEAKER) return - - val constraintLayout = - binding.root.findViewById(R.id.conference_constraint_layout) - ?: return - val set = getConstraintSet(constraintLayout) - - set.connect( - R.id.local_participant_background, - ConstraintSet.BOTTOM, - R.id.hinge_bottom, - ConstraintSet.BOTTOM, - 0 - ) - set.connect( - R.id.local_participant_background, - ConstraintSet.END, - R.id.conference_constraint_layout, - ConstraintSet.END, - 0 - ) - set.connect( - R.id.local_participant_background, - ConstraintSet.TOP, - R.id.top_barrier, - ConstraintSet.BOTTOM, - 0 - ) - set.connect( - R.id.local_participant_background, - ConstraintSet.START, - R.id.conference_constraint_layout, - ConstraintSet.START, - 0 - ) - - set.constrainWidth(R.id.local_participant_background, 0) - set.constrainHeight(R.id.local_participant_background, 0) - set.constrainWidth(R.id.local_participant_avatar, 0) - set.constrainHeight(R.id.local_participant_avatar, 0) - - Log.i("[Conference Call] Updating active speaker layout for 1 participant (myself)") - if (corePreferences.enableAnimations) { - animateConstraintLayout(constraintLayout, set) - } else { - set.applyTo(constraintLayout) - } - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceLayoutFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceLayoutFragment.kt deleted file mode 100644 index f840c99ac..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceLayoutFragment.kt +++ /dev/null @@ -1,97 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.os.Bundle -import android.view.View -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.navigation.navGraphViewModels -import org.linphone.R -import org.linphone.activities.voip.ConferenceDisplayMode -import org.linphone.activities.voip.viewmodels.ConferenceViewModel -import org.linphone.activities.voip.viewmodels.ControlsViewModel -import org.linphone.databinding.VoipConferenceLayoutFragmentBinding - -class ConferenceLayoutFragment : GenericVideoPreviewFragment() { - private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph) - private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph) - - override fun getLayoutId(): Int = R.layout.voip_conference_layout_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.conferenceViewModel = conferenceViewModel - - binding.controlsViewModel = controlsViewModel - - binding.setCancelClickListener { - goBack() - } - - conferenceViewModel.conferenceParticipantDevices.observe( - viewLifecycleOwner - ) { - if (it.size > conferenceViewModel.maxParticipantsForMosaicLayout && conferenceViewModel.conferenceDisplayMode.value == ConferenceDisplayMode.GRID) { - showTooManyParticipantsForMosaicLayoutDialog() - } - } - - conferenceViewModel.conferenceDisplayMode.observe( - viewLifecycleOwner - ) { - binding.localPreviewVideoSurface.visibility = if (it == ConferenceDisplayMode.AUDIO_ONLY) { - View.GONE - } else { - View.VISIBLE - } - } - - binding.setDismissDialogClickListener { - val dialog = binding.root.findViewById( - R.id.too_many_participants_dialog - ) - dialog?.visibility = View.GONE - } - } - - override fun onResume() { - super.onResume() - - if (conferenceViewModel.conferenceParticipantDevices.value.orEmpty().size > conferenceViewModel.maxParticipantsForMosaicLayout) { - showTooManyParticipantsForMosaicLayoutDialog() - } - - setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera) - } - - override fun onPause() { - super.onPause() - - cleanUpLocalVideoPreview(binding.localPreviewVideoSurface) - } - - private fun showTooManyParticipantsForMosaicLayoutDialog() { - val dialog = binding.root.findViewById(R.id.too_many_participants_dialog) - dialog?.visibility = View.VISIBLE - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceParticipantsFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceParticipantsFragment.kt deleted file mode 100644 index c0acf62fa..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/ConferenceParticipantsFragment.kt +++ /dev/null @@ -1,99 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.os.Bundle -import android.view.View -import android.widget.Toast -import androidx.navigation.navGraphViewModels -import org.linphone.R -import org.linphone.activities.navigateToAddParticipants -import org.linphone.activities.voip.viewmodels.ConferenceViewModel -import org.linphone.activities.voip.viewmodels.ControlsViewModel -import org.linphone.core.tools.Log -import org.linphone.databinding.VoipConferenceParticipantsFragmentBinding - -class ConferenceParticipantsFragment : GenericVideoPreviewFragment() { - private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph) - private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph) - - // Only display events happening during while this fragment is visible - private var skipEvents = true - - override fun getLayoutId(): Int = R.layout.voip_conference_participants_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.conferenceViewModel = conferenceViewModel - - binding.controlsViewModel = controlsViewModel - - conferenceViewModel.conferenceExists.observe( - viewLifecycleOwner - ) { exists -> - if (!exists) { - Log.w("[Conference Participants] Conference no longer exists, going back") - goBack() - } - } - - conferenceViewModel.participantAdminStatusChangedEvent.observe( - viewLifecycleOwner - ) { - it.consume { participantData -> - val participantName = - participantData.contact.value?.name ?: participantData.displayName.value - val message = if (participantData.participant.isAdmin) { - getString(R.string.conference_admin_set).format(participantName) - } else { - getString(R.string.conference_admin_unset).format(participantName) - } - if (!skipEvents) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } - } - } - - binding.setCancelClickListener { - goBack() - } - - binding.setEditClickListener { - navigateToAddParticipants() - } - } - - override fun onResume() { - super.onResume() - - skipEvents = false - setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera) - } - - override fun onPause() { - super.onPause() - - skipEvents = true - cleanUpLocalVideoPreview(binding.localPreviewVideoSurface) - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/GenericVideoPreviewFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/GenericVideoPreviewFragment.kt deleted file mode 100644 index 98372c58e..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/GenericVideoPreviewFragment.kt +++ /dev/null @@ -1,78 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.view.MotionEvent -import android.view.TextureView -import android.view.View -import android.widget.ImageView -import androidx.databinding.ViewDataBinding -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.GenericFragment - -abstract class GenericVideoPreviewFragment : GenericFragment() { - private var previewX: Float = 0f - private var previewY: Float = 0f - private var switchX: Float = 0f - private var switchY: Float = 0f - - private var switchCameraImageView: ImageView? = null - - private val previewTouchListener = View.OnTouchListener { view, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - previewX = view.x - event.rawX - previewY = view.y - event.rawY - switchX = (switchCameraImageView?.x ?: 0f) - event.rawX - switchY = (switchCameraImageView?.y ?: 0f) - event.rawY - true - } - MotionEvent.ACTION_MOVE -> { - view.animate() - .x(event.rawX + previewX) - .y(event.rawY + previewY) - .setDuration(0) - .start() - switchCameraImageView?.apply { - animate() - .x(event.rawX + switchX) - .y(event.rawY + switchY) - .setDuration(0) - .start() - } - true - } - else -> { - view.performClick() - false - } - } - } - - protected fun setupLocalVideoPreview(localVideoPreview: TextureView, switchCamera: ImageView?) { - switchCameraImageView = switchCamera - localVideoPreview.setOnTouchListener(previewTouchListener) - coreContext.core.nativePreviewWindowId = localVideoPreview - } - - protected fun cleanUpLocalVideoPreview(localVideoPreview: TextureView) { - localVideoPreview.setOnTouchListener(null) - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/IncomingCallFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/IncomingCallFragment.kt deleted file mode 100644 index b10a04710..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/IncomingCallFragment.kt +++ /dev/null @@ -1,95 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.os.Bundle -import android.os.SystemClock -import android.view.View -import android.widget.Chronometer -import androidx.navigation.navGraphViewModels -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.navigateToActiveCall -import org.linphone.activities.voip.viewmodels.CallsViewModel -import org.linphone.activities.voip.viewmodels.ControlsViewModel -import org.linphone.core.tools.Log -import org.linphone.databinding.VoipCallIncomingFragmentBinding - -class IncomingCallFragment : GenericFragment() { - private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph) - private val callsViewModel: CallsViewModel by navGraphViewModels(R.id.call_nav_graph) - - override fun getLayoutId(): Int = R.layout.voip_call_incoming_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.controlsViewModel = controlsViewModel - - binding.callsViewModel = callsViewModel - - callsViewModel.callConnectedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToActiveCall() - } - } - - callsViewModel.callEndedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToActiveCall() - } - } - - callsViewModel.currentCallData.observe( - viewLifecycleOwner - ) { - if (it != null) { - val timer = binding.root.findViewById(R.id.incoming_call_timer) - timer.base = - SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds - timer.start() - } - } - - val earlyMediaVideo = arguments?.getBoolean("earlyMediaVideo") ?: false - if (earlyMediaVideo) { - Log.i("[Incoming Call] Video early media detected, setting native window id") - coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface - } - } - - // We don't want the proximity sensor to turn screen OFF in this fragment - override fun onResume() { - super.onResume() - controlsViewModel.forceDisableProximitySensor.value = true - } - - override fun onPause() { - controlsViewModel.forceDisableProximitySensor.value = false - super.onPause() - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt deleted file mode 100644 index 8ba1baa55..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/OutgoingCallFragment.kt +++ /dev/null @@ -1,100 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.os.Bundle -import android.os.SystemClock -import android.view.View -import android.widget.Chronometer -import androidx.navigation.navGraphViewModels -import org.linphone.R -import org.linphone.activities.navigateToActiveCall -import org.linphone.activities.voip.viewmodels.CallsViewModel -import org.linphone.activities.voip.viewmodels.ControlsViewModel -import org.linphone.databinding.VoipCallOutgoingFragmentBinding - -class OutgoingCallFragment : GenericVideoPreviewFragment() { - private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph) - private val callsViewModel: CallsViewModel by navGraphViewModels(R.id.call_nav_graph) - - override fun getLayoutId(): Int = R.layout.voip_call_outgoing_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - - binding.controlsViewModel = controlsViewModel - - binding.callsViewModel = callsViewModel - - callsViewModel.callConnectedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToActiveCall() - } - } - - callsViewModel.callEndedEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToActiveCall() - } - } - - callsViewModel.currentCallData.observe( - viewLifecycleOwner - ) { - if (it != null) { - val timer = binding.root.findViewById(R.id.outgoing_call_timer) - timer.base = - SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds - timer.start() - } - } - - controlsViewModel.isOutgoingEarlyMedia.observe( - viewLifecycleOwner - ) { - if (it) { - setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera) - } - } - } - - // We don't want the proximity sensor to turn screen OFF in this fragment - override fun onResume() { - super.onResume() - - controlsViewModel.forceDisableProximitySensor.value = true - if (controlsViewModel.isOutgoingEarlyMedia.value == true) { - setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera) - } - } - - override fun onPause() { - super.onPause() - - controlsViewModel.forceDisableProximitySensor.value = false - cleanUpLocalVideoPreview(binding.localPreviewVideoSurface) - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/SingleCallFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/SingleCallFragment.kt deleted file mode 100644 index 2f90854af..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/SingleCallFragment.kt +++ /dev/null @@ -1,265 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.app.Dialog -import android.content.Intent -import android.os.Bundle -import android.os.SystemClock -import android.view.View -import android.widget.Chronometer -import androidx.constraintlayout.widget.ConstraintSet -import androidx.navigation.navGraphViewModels -import androidx.window.layout.FoldingFeature -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.* -import org.linphone.activities.main.MainActivity -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.voip.viewmodels.CallsViewModel -import org.linphone.activities.voip.viewmodels.ConferenceViewModel -import org.linphone.activities.voip.viewmodels.ControlsViewModel -import org.linphone.activities.voip.viewmodels.StatisticsListViewModel -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.databinding.VoipSingleCallFragmentBinding -import org.linphone.utils.AppUtils -import org.linphone.utils.DialogUtils - -class SingleCallFragment : GenericVideoPreviewFragment() { - private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph) - private val callsViewModel: CallsViewModel by navGraphViewModels(R.id.call_nav_graph) - private val conferenceViewModel: ConferenceViewModel by navGraphViewModels(R.id.call_nav_graph) - private val statsViewModel: StatisticsListViewModel by navGraphViewModels(R.id.call_nav_graph) - - private var dialog: Dialog? = null - - override fun getLayoutId(): Int = R.layout.voip_single_call_fragment - - override fun onStart() { - useMaterialSharedAxisXForwardAnimation = false - - super.onStart() - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - controlsViewModel.hideCallStats() // In case it was toggled on during incoming/outgoing fragment was visible - - binding.lifecycleOwner = viewLifecycleOwner - - binding.controlsViewModel = controlsViewModel - - binding.callsViewModel = callsViewModel - - binding.conferenceViewModel = conferenceViewModel - - binding.statsViewModel = statsViewModel - - callsViewModel.currentCallData.observe( - viewLifecycleOwner - ) { callData -> - if (callData != null) { - val call = callData.call - when (val callState = call.state) { - Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> { - Log.i( - "[Single Call] New current call is in [$callState] state, switching to IncomingCall fragment" - ) - navigateToIncomingCall() - } - Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> { - Log.i( - "[Single Call] New current call is in [$callState] state, switching to OutgoingCall fragment" - ) - navigateToOutgoingCall() - } - else -> { - Log.i( - "[Single Call] New current call is in [$callState] state, updating call UI" - ) - val timer = binding.root.findViewById(R.id.active_call_timer) - timer.base = - SystemClock.elapsedRealtime() - (1000 * call.duration) // Linphone timestamps are in seconds - timer.start() - - if (corePreferences.enableFullScreenWhenJoiningVideoCall) { - if (call.currentParams.isVideoEnabled) { - Log.i( - "[Single Call] Call params have video enabled, enabling full screen mode" - ) - controlsViewModel.fullScreenMode.value = true - } - } - } - } - } - } - - controlsViewModel.goToConferenceParticipantsListEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToConferenceParticipants() - } - } - - controlsViewModel.goToChatEvent.observe( - viewLifecycleOwner - ) { - it.consume { - goToChat() - } - } - - controlsViewModel.goToCallsListEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToCallsList() - } - } - - controlsViewModel.goToConferenceLayoutSettingsEvent.observe( - viewLifecycleOwner - ) { - it.consume { - navigateToConferenceLayout() - } - } - - controlsViewModel.foldingState.observe( - viewLifecycleOwner - ) { feature -> - updateHingeRelatedConstraints(feature) - } - - callsViewModel.callUpdateEvent.observe( - viewLifecycleOwner - ) { - it.consume { call -> - if (call.state == Call.State.StreamsRunning) { - dialog?.dismiss() - } else if (call.state == Call.State.UpdatedByRemote) { - if (coreContext.core.isVideoEnabled) { - val remoteVideo = call.remoteParams?.isVideoEnabled ?: false - val localVideo = call.currentParams.isVideoEnabled - if (remoteVideo && !localVideo) { - showCallVideoUpdateDialog(call) - } - } else { - Log.w( - "[Single Call] Video display & capture are disabled, don't show video dialog" - ) - } - } - } - } - - controlsViewModel.goToDialerEvent.observe( - viewLifecycleOwner - ) { - it.consume { isCallTransfer -> - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - if (corePreferences.skipDialerForNewCallAndTransfer) { - intent.putExtra("Contacts", true) - } else { - intent.putExtra("Dialer", true) - } - intent.putExtra("Transfer", isCallTransfer) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } - } - } - - override fun onResume() { - super.onResume() - - coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface - setupLocalVideoPreview(binding.localPreviewVideoSurface, binding.switchCamera) - } - - override fun onPause() { - super.onPause() - - controlsViewModel.hideExtraButtons(true) - cleanUpLocalVideoPreview(binding.localPreviewVideoSurface) - } - - private fun showCallVideoUpdateDialog(call: Call) { - val viewModel = DialogViewModel( - AppUtils.getString(R.string.call_video_update_requested_dialog) - ) - dialog = DialogUtils.getVoipDialog(requireContext(), viewModel) - - viewModel.showCancelButton( - { - coreContext.answerCallVideoUpdateRequest(call, false) - dialog?.dismiss() - }, - getString(R.string.dialog_decline) - ) - - viewModel.showOkButton( - { - coreContext.answerCallVideoUpdateRequest(call, true) - dialog?.dismiss() - }, - getString(R.string.dialog_accept) - ) - - dialog?.show() - } - - private fun goToChat() { - val intent = Intent() - intent.setClass(requireContext(), MainActivity::class.java) - intent.putExtra("Chat", true) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - } - - private fun updateHingeRelatedConstraints(feature: FoldingFeature) { - Log.i("[Single Call] Updating constraint layout hinges: $feature") - - val constraintLayout = binding.constraintLayout - val set = ConstraintSet() - set.clone(constraintLayout) - - // Only modify UI in table top mode - if (feature.orientation == FoldingFeature.Orientation.HORIZONTAL && - feature.state == FoldingFeature.State.HALF_OPENED - ) { - set.setGuidelinePercent(R.id.hinge_top, 0.5f) - set.setGuidelinePercent(R.id.hinge_bottom, 0.5f) - controlsViewModel.folded.value = true - } else { - set.setGuidelinePercent(R.id.hinge_top, 0f) - set.setGuidelinePercent(R.id.hinge_bottom, 1f) - controlsViewModel.folded.value = false - } - - set.applyTo(constraintLayout) - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/fragments/StatusFragment.kt b/app/src/main/java/org/linphone/activities/voip/fragments/StatusFragment.kt deleted file mode 100644 index 0536d1da6..000000000 --- a/app/src/main/java/org/linphone/activities/voip/fragments/StatusFragment.kt +++ /dev/null @@ -1,174 +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 . - */ -package org.linphone.activities.voip.fragments - -import android.app.Dialog -import android.os.Bundle -import android.view.View -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.navGraphViewModels -import java.util.* -import org.linphone.R -import org.linphone.activities.GenericFragment -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.activities.voip.viewmodels.ControlsViewModel -import org.linphone.activities.voip.viewmodels.StatusViewModel -import org.linphone.core.Call -import org.linphone.core.tools.Log -import org.linphone.databinding.VoipStatusFragmentBinding -import org.linphone.utils.DialogUtils - -class StatusFragment : GenericFragment() { - private lateinit var viewModel: StatusViewModel - private val controlsViewModel: ControlsViewModel by navGraphViewModels(R.id.call_nav_graph) - - private var zrtpDialog: Dialog? = null - - override fun getLayoutId(): Int = R.layout.voip_status_fragment - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - binding.lifecycleOwner = viewLifecycleOwner - useMaterialSharedAxisXForwardAnimation = false - - viewModel = ViewModelProvider(this)[StatusViewModel::class.java] - binding.viewModel = viewModel - - binding.setRefreshClickListener { - viewModel.refreshRegister() - } - - viewModel.showZrtpDialogEvent.observe( - viewLifecycleOwner - ) { - it.consume { call -> - if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) { - showZrtpDialog(call) - } - } - } - - viewModel.showCallStatsEvent.observe( - viewLifecycleOwner - ) { - it.consume { - controlsViewModel.showCallStats(skipAnimation = true) - } - } - } - - override fun onDestroy() { - if (zrtpDialog != null) { - zrtpDialog?.dismiss() - } - super.onDestroy() - } - - private fun showZrtpDialog(call: Call) { - if (zrtpDialog != null && zrtpDialog?.isShowing == true) { - Log.w( - "[Status Fragment] ZRTP dialog already visible, closing it and creating a new one" - ) - zrtpDialog?.dismiss() - zrtpDialog = null - } - - val token = call.authenticationToken - if (token == null || token.length < 4) { - Log.e("[Status Fragment] ZRTP token is invalid: $token") - return - } - - val toRead: String - val toListen: String - when (call.dir) { - Call.Dir.Incoming -> { - toRead = token.substring(0, 2) - toListen = token.substring(2) - } - else -> { - toRead = token.substring(2) - toListen = token.substring(0, 2) - } - } - - val viewModel = DialogViewModel( - getString(R.string.zrtp_dialog_explanation), - getString(R.string.zrtp_dialog_title) - ) - viewModel.showZrtp = true - viewModel.zrtpReadSas = toRead.uppercase(Locale.getDefault()) - viewModel.zrtpListenSas = toListen.uppercase(Locale.getDefault()) - viewModel.showIcon = true - viewModel.iconResource = if (call.audioStats?.isZrtpKeyAgreementAlgoPostQuantum == true) { - R.drawable.security_post_quantum - } else { - R.drawable.security_2_indicator - } - - val dialog: Dialog = DialogUtils.getVoipDialog(requireContext(), viewModel) - - viewModel.showCancelButton( - { - if (call.state != Call.State.End && call.state != Call.State.Released) { - if (call.authenticationTokenVerified) { - Log.w( - "[Status Fragment] Removing trust from previously verified ZRTP SAS auth token" - ) - this@StatusFragment.viewModel.previouslyDeclineToken = true - call.authenticationTokenVerified = false - } - } else { - Log.e( - "[Status Fragment] Can't decline the ZRTP SAS token, call is in state [${call.state}]" - ) - } - dialog.dismiss() - zrtpDialog = null - }, - getString(R.string.zrtp_dialog_later_button_label) - ) - - viewModel.showOkButton( - { - if (call.state != Call.State.End && call.state != Call.State.Released) { - call.authenticationTokenVerified = true - } else { - Log.e( - "[Status Fragment] Can't verify the ZRTP SAS token, call is in state [${call.state}]" - ) - } - dialog.dismiss() - zrtpDialog = null - }, - getString(R.string.zrtp_dialog_correct_button_label) - ) - - viewModel.dismissEvent.observe(viewLifecycleOwner) { - it.consume { - dialog.dismiss() - } - } - - zrtpDialog = dialog - dialog.show() - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/CallsViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/CallsViewModel.kt deleted file mode 100644 index d12d0ce90..000000000 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/CallsViewModel.kt +++ /dev/null @@ -1,330 +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 . - */ -package org.linphone.activities.voip.viewmodels - -import android.Manifest -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.voip.data.CallData -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils -import org.linphone.utils.Event -import org.linphone.utils.PermissionHelper - -class CallsViewModel : ViewModel() { - val currentCallData = MutableLiveData() - - val callsData = MutableLiveData>() - - val inactiveCallsCount = MutableLiveData() - - val currentCallUnreadChatMessageCount = MutableLiveData() - - val chatAndCallsCount = MediatorLiveData() - - val isMicrophoneMuted = MutableLiveData() - - val isMuteMicrophoneEnabled = MutableLiveData() - - val callConnectedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val callEndedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val callUpdateEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val noMoreCallEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val askPermissionEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val listener = object : CoreListenerStub() { - override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { - updateUnreadChatCount() - } - - override fun onMessagesReceived( - core: Core, - chatRoom: ChatRoom, - messages: Array - ) { - updateUnreadChatCount() - } - - override fun onLastCallEnded(core: Core) { - Log.i("[Calls] Last call ended") - currentCallData.value?.destroy() - noMoreCallEvent.value = Event(true) - } - - override fun onCallStateChanged(core: Core, call: Call, state: Call.State, message: String) { - Log.i("[Calls] Call with ID [${call.callLog.callId}] state changed: $state") - - if (state == Call.State.IncomingEarlyMedia || state == Call.State.IncomingReceived || state == Call.State.OutgoingInit) { - if (!callDataAlreadyExists(call)) { - addCallToList(call) - } - } - - val currentCall = core.currentCall - Log.i("[Calls] Current call is ${currentCall?.remoteAddress?.asStringUriOnly()}") - if (currentCall != null && currentCallData.value?.call != currentCall) { - updateCurrentCallData(currentCall) - } else if (currentCall == null && core.callsNb > 0) { - updateCurrentCallData(null) - } else if (currentCallData.value == null) { - updateCurrentCallData(currentCall) - } - - if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { - removeCallFromList(call) - if (core.callsNb > 0) { - Log.i( - "[Calls] Call has ended but there are still at least one other existing call" - ) - callEndedEvent.value = Event(call) - } - } else if (call.state == Call.State.UpdatedByRemote) { - // If the correspondent asks to turn on video while audio call, - // defer update until user has chosen whether to accept it or not - val remoteVideo = call.remoteParams?.isVideoEnabled ?: false - val localVideo = call.currentParams.isVideoEnabled - val autoAccept = call.core.videoActivationPolicy.automaticallyAccept - if (remoteVideo && !localVideo && !autoAccept) { - if (coreContext.core.isVideoCaptureEnabled || coreContext.core.isVideoDisplayEnabled) { - call.deferUpdate() - callUpdateEvent.value = Event(call) - } else { - coreContext.answerCallVideoUpdateRequest(call, false) - } - } - } else if (state == Call.State.Connected) { - callConnectedEvent.value = Event(call) - } else if (state == Call.State.StreamsRunning) { - callUpdateEvent.value = Event(call) - } - - updateInactiveCallsCount() - } - } - - init { - coreContext.core.addListener(listener) - - val currentCall = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() - if (currentCall != null) { - Log.i( - "[Calls] Initializing ViewModel using call [${currentCall.remoteAddress.asStringUriOnly()}] as current" - ) - currentCallData.value?.destroy() - - val viewModel = CallData(currentCall) - currentCallData.value = viewModel - } - - chatAndCallsCount.value = 0 - chatAndCallsCount.addSource(inactiveCallsCount) { - chatAndCallsCount.value = updateCallsAndChatCount() - } - chatAndCallsCount.addSource(currentCallUnreadChatMessageCount) { - chatAndCallsCount.value = updateCallsAndChatCount() - } - - initCallList() - updateInactiveCallsCount() - updateUnreadChatCount() - updateMicState() - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - - currentCallData.value?.destroy() - callsData.value.orEmpty().forEach(CallData::destroy) - - super.onCleared() - } - - fun toggleMuteMicrophone() { - if (!PermissionHelper.get().hasRecordAudioPermission()) { - askPermissionEvent.value = Event(Manifest.permission.RECORD_AUDIO) - return - } - - val call = currentCallData.value?.call - if (call != null && call.conference != null) { - val micMuted = call.conference?.microphoneMuted ?: false - call.conference?.microphoneMuted = !micMuted - } else { - val micMuted = call?.microphoneMuted ?: false - call?.microphoneMuted = !micMuted - } - updateMicState() - } - - fun mergeCallsIntoConference() { - Log.i("[Calls] Merging all calls into new conference") - val core = coreContext.core - val params = core.createConferenceParams(null) - params.subject = AppUtils.getString(R.string.conference_local_title) - // Prevent group call to start in audio only layout - params.isVideoEnabled = true - val conference = core.createConferenceWithParams(params) - conference?.addParticipants(core.calls) - } - - private fun initCallList() { - val calls = arrayListOf() - - for (call in coreContext.core.calls) { - val data: CallData = if (currentCallData.value?.call == call) { - currentCallData.value!! - } else { - CallData(call) - } - Log.i("[Calls] Adding call with ID ${call.callLog.callId} to calls list") - calls.add(data) - } - - callsData.value = calls - } - - private fun addCallToList(call: Call) { - Log.i("[Calls] Adding call with ID ${call.callLog.callId} to calls list") - - val calls = arrayListOf() - calls.addAll(callsData.value.orEmpty()) - - val data = CallData(call) - calls.add(data) - - callsData.value = calls - } - - private fun removeCallFromList(call: Call) { - Log.i("[Calls] Removing call with ID ${call.callLog.callId} from calls list") - - val calls = arrayListOf() - for (data in callsData.value.orEmpty()) { - if (data.call == call) { - data.destroy() - } else { - calls.add(data) - } - } - - callsData.value = calls - } - - private fun updateCurrentCallData(currentCall: Call?) { - var callToUse = currentCall - if (currentCall == null) { - if (coreContext.core.callsNb == 1) { - // Make sure the current call data is matching the only call - val firstData = callsData.value?.firstOrNull() - if (firstData != null && currentCallData.value != firstData) { - Log.i( - "[Calls] Only one call in Core and the current call data doesn't match it, updating it" - ) - currentCallData.value = firstData!! - } - return - } - - val firstCall = coreContext.core.calls.find { call -> - call.state != Call.State.Error && call.state != Call.State.End && call.state != Call.State.Released - } - if (firstCall != null && currentCallData.value?.call != firstCall) { - Log.i( - "[Calls] Using [${firstCall.remoteAddress.asStringUriOnly()}] call as \"current\" call" - ) - callToUse = firstCall - } - } - - if (callToUse == null) { - Log.w("[Calls] No call found to be used as \"current\"") - return - } - - var found = false - for (callData in callsData.value.orEmpty()) { - if (callData.call == callToUse) { - Log.i( - "[Calls] Updating current call to: ${callData.call.remoteAddress.asStringUriOnly()}" - ) - currentCallData.value = callData - found = true - break - } - } - if (!found) { - Log.w( - "[Calls] Call with ID [${callToUse.callLog.callId}] not found in calls data list, shouldn't happen!" - ) - val viewModel = CallData(callToUse) - currentCallData.value = viewModel - } - - updateMicState() - // updateUnreadChatCount() - } - - private fun callDataAlreadyExists(call: Call): Boolean { - for (callData in callsData.value.orEmpty()) { - if (callData.call == call) { - return true - } - } - return false - } - - fun updateMicState() { - isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || currentCallData.value?.call?.microphoneMuted == true - isMuteMicrophoneEnabled.value = currentCallData.value?.call != null - } - - private fun updateCallsAndChatCount(): Int { - return (inactiveCallsCount.value ?: 0) + (currentCallUnreadChatMessageCount.value ?: 0) - } - - private fun updateUnreadChatCount() { - // For now we don't display in-call chat, so use global unread chat messages count - currentCallUnreadChatMessageCount.value = coreContext.core.unreadChatMessageCountFromActiveLocals - } - - private fun updateInactiveCallsCount() { - // TODO: handle local conference - val callsNb = coreContext.core.callsNb - inactiveCallsCount.value = if (callsNb > 0) callsNb - 1 else 0 - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceParticipantsViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceParticipantsViewModel.kt deleted file mode 100644 index a3dc7aa9c..000000000 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceParticipantsViewModel.kt +++ /dev/null @@ -1,81 +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 . - */ -package org.linphone.activities.voip.viewmodels - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import org.linphone.contact.ContactsSelectionViewModel -import org.linphone.core.Address -import org.linphone.core.Conference -import org.linphone.core.tools.Log - -class ConferenceParticipantsViewModelFactory(private val conference: Conference) : - ViewModelProvider.NewInstanceFactory() { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return ConferenceParticipantsViewModel(conference) as T - } -} - -class ConferenceParticipantsViewModel(val conference: Conference) : ContactsSelectionViewModel() { - init { - selectCurrentParticipants() - } - - fun applyChanges() { - // Adding new participants first, because if we remove all of them (or all of them except one) - // It will terminate the conference first and we won't be able to add new participants after - for (address in selectedAddresses.value.orEmpty()) { - val participant = conference.participantList.find { participant -> - participant.address.weakEqual(address) - } - if (participant == null) { - Log.i( - "[Conference Participants] Participant ${address.asStringUriOnly()} will be added to group" - ) - conference.addParticipant(address) - } - } - - // Removing participants - for (participant in conference.participantList) { - val member = selectedAddresses.value.orEmpty().find { address -> - participant.address.weakEqual(address) - } - if (member == null) { - Log.w( - "[Conference Participants] Participant ${participant.address.asStringUriOnly()} will be removed from conference" - ) - conference.removeParticipant(participant) - } - } - } - - private fun selectCurrentParticipants() { - val list = arrayListOf
() - - for (participant in conference.participantList) { - list.add(participant.address) - } - - selectedAddresses.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt deleted file mode 100644 index 14f41f2ce..000000000 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/ConferenceViewModel.kt +++ /dev/null @@ -1,772 +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 . - */ -package org.linphone.activities.voip.viewmodels - -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.voip.ConferenceDisplayMode -import org.linphone.activities.voip.data.ConferenceParticipantData -import org.linphone.activities.voip.data.ConferenceParticipantDeviceData -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 ConferenceViewModel : ViewModel() { - val conferenceExists = MutableLiveData() - val subject = MutableLiveData() - val isConferenceLocallyPaused = MutableLiveData() - val isVideoConference = MutableLiveData() - val isMeAdmin = MutableLiveData() - - val conference = MutableLiveData() - val conferenceCreationPending = MutableLiveData() - val conferenceParticipants = MutableLiveData>() - val conferenceParticipantDevices = MutableLiveData>() - val conferenceDisplayMode = MutableLiveData() - val activeSpeakerConferenceParticipantDevices = MediatorLiveData>() - - val isRecording = MutableLiveData() - val isRemotelyRecorded = MutableLiveData() - - val maxParticipantsForMosaicLayout = corePreferences.maxConferenceParticipantsForMosaicLayout - - val twoOrMoreParticipants = MutableLiveData() - val moreThanTwoParticipants = MutableLiveData() - - val speakingParticipantFound = MutableLiveData() - val speakingParticipant = MutableLiveData() - val speakingParticipantVideoEnabled = MutableLiveData() - val meParticipant = MutableLiveData() - - val isBroadcast = MutableLiveData() - val isMeListenerOnly = MutableLiveData() - - val participantAdminStatusChangedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val firstToJoinEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val allParticipantsLeftEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val secondParticipantJoinedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val moreThanTwoParticipantsJoinedEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private var waitForNextStreamsRunningToUpdateLayout = false - - val reloadConferenceFragmentEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val conferenceListener = object : ConferenceListenerStub() { - override fun onParticipantAdded(conference: Conference, participant: Participant) { - Log.i("[Conference] Participant added: ${participant.address.asStringUriOnly()}") - updateParticipantsList(conference) - } - - override fun onParticipantRemoved(conference: Conference, participant: Participant) { - Log.i("[Conference] Participant removed: ${participant.address.asStringUriOnly()}") - updateParticipantsList(conference) - } - - override fun onParticipantDeviceAdded( - conference: Conference, - participantDevice: ParticipantDevice - ) { - Log.i( - "[Conference] Participant device added: ${participantDevice.address.asStringUriOnly()}" - ) - addParticipantDevice(conference, participantDevice) - - if (conferenceParticipantDevices.value.orEmpty().size == 2) { - secondParticipantJoinedEvent.value = Event(true) - } else if (conferenceParticipantDevices.value.orEmpty().size > 2) { - moreThanTwoParticipantsJoinedEvent.value = Event(true) - } - } - - override fun onParticipantDeviceRemoved( - conference: Conference, - participantDevice: ParticipantDevice - ) { - Log.i( - "[Conference] Participant device removed: ${participantDevice.address.asStringUriOnly()}" - ) - removeParticipantDevice(participantDevice) - - when (conferenceParticipantDevices.value.orEmpty().size) { - 1 -> { - speakingParticipant.value?.videoEnabled?.value = false - speakingParticipantVideoEnabled.value = false - allParticipantsLeftEvent.value = Event(true) - } - 2 -> { - secondParticipantJoinedEvent.value = Event(true) - } - else -> {} - } - } - - override fun onParticipantAdminStatusChanged( - conference: Conference, - participant: Participant - ) { - Log.i( - "[Conference] Participant admin status changed [${participant.address.asStringUriOnly()}] is ${if (participant.isAdmin) "now admin" else "no longer admin"}" - ) - isMeAdmin.value = conference.me.isAdmin - updateParticipantsList(conference) - - if (conference.me.address.weakEqual(participant.address)) { - Log.i( - "[Conference] Found me participant [${participant.address.asStringUriOnly()}]" - ) - val participantData = ConferenceParticipantData(conference, participant) - participantAdminStatusChangedEvent.value = Event(participantData) - return - } - - val participantData = conferenceParticipants.value.orEmpty().find { data -> - data.participant.address.weakEqual( - participant.address - ) - } - if (participantData != null) { - participantAdminStatusChangedEvent.value = Event(participantData) - } else { - Log.w( - "[Conference] Failed to find participant [${participant.address.asStringUriOnly()}] in conferenceParticipants list" - ) - } - } - - override fun onSubjectChanged(conference: Conference, subject: String) { - Log.i("[Conference] Subject changed: $subject") - this@ConferenceViewModel.subject.value = subject - } - - override fun onParticipantDeviceStateChanged( - conference: Conference, - device: ParticipantDevice, - state: ParticipantDevice.State - ) { - if (conference.isMe(device.address)) { - when (state) { - ParticipantDevice.State.Present -> { - Log.i("[Conference] Entered conference") - isConferenceLocallyPaused.value = false - } - ParticipantDevice.State.OnHold -> { - Log.i("[Conference] Left conference") - isConferenceLocallyPaused.value = true - } - else -> {} - } - } else { - speakingParticipantVideoEnabled.value = speakingParticipant.value?.isInConference?.value == true && speakingParticipant.value?.isSendingVideo?.value == true - } - } - - override fun onActiveSpeakerParticipantDevice( - conference: Conference, - participantDevice: ParticipantDevice - ) { - Log.i( - "[Conference] Participant [${participantDevice.address.asStringUriOnly()}] is currently being displayed as active speaker" - ) - val device = conferenceParticipantDevices.value.orEmpty().find { - it.participantDevice.address.weakEqual(participantDevice.address) - } - - if (device != null && device != speakingParticipant.value) { - Log.i("[Conference] Found actively speaking participant device") - speakingParticipant.value?.isActiveSpeaker?.value = false - device.isActiveSpeaker.value = true - speakingParticipant.value = device!! - speakingParticipantFound.value = true - speakingParticipantVideoEnabled.value = speakingParticipant.value?.isInConference?.value == true && speakingParticipant.value?.isSendingVideo?.value == true - } else if (device == null) { - Log.w( - "[Conference] Participant device [${participantDevice.address.asStringUriOnly()}] is the active speaker but couldn't find it in devices list" - ) - } - } - - override fun onParticipantDeviceMediaAvailabilityChanged( - conference: Conference, - device: ParticipantDevice - ) { - speakingParticipantVideoEnabled.value = speakingParticipant.value?.isInConference?.value == true && speakingParticipant.value?.isSendingVideo?.value == true - } - - override fun onParticipantDeviceMediaCapabilityChanged( - conference: Conference, - device: ParticipantDevice - ) { - speakingParticipantVideoEnabled.value = speakingParticipant.value?.isInConference?.value == true && speakingParticipant.value?.isSendingVideo?.value == true - } - - override fun onStateChanged(conference: Conference, state: Conference.State) { - Log.i("[Conference] State changed: $state") - isVideoConference.value = conference.currentParams.isVideoEnabled && !corePreferences.disableVideo - - when (state) { - Conference.State.Created -> { - configureConference(conference) - } - Conference.State.TerminationPending -> { - terminateConference(conference) - } - else -> {} - } - } - } - - private val listener = object : CoreListenerStub() { - override fun onConferenceStateChanged( - core: Core, - conference: Conference, - state: Conference.State - ) { - Log.i("[Conference] Conference state changed: $state") - if (state == Conference.State.Instantiated) { - conferenceCreationPending.value = true - initConference(conference) - } else if (state == Conference.State.Created) { - if (conferenceCreationPending.value == true) { - conferenceCreationPending.value = false - } - } - } - - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State?, - message: String - ) { - if (state == Call.State.StreamsRunning && waitForNextStreamsRunningToUpdateLayout) { - waitForNextStreamsRunningToUpdateLayout = false - reloadConferenceFragmentEvent.value = Event(true) - } - if (state == Call.State.StreamsRunning && call.conference?.isIn == true) { - isConferenceLocallyPaused.value = false - conferenceParticipantDevices.value?.forEach { - if (it.isMe) { - it.isInConference.value = true - } - } - } - } - } - - init { - coreContext.core.addListener(listener) - conferenceExists.value = false - - conferenceParticipants.value = arrayListOf() - conferenceParticipantDevices.value = arrayListOf() - activeSpeakerConferenceParticipantDevices.addSource(conferenceParticipantDevices) { - activeSpeakerConferenceParticipantDevices.value = conferenceParticipantDevices.value.orEmpty().drop( - 1 - ) - } - - subject.value = AppUtils.getString(R.string.conference_default_title) - - var conference = coreContext.core.conference ?: coreContext.core.currentCall?.conference - if (conference == null) { - for (call in coreContext.core.calls) { - if (call.conference != null) { - conference = call.conference - break - } - } - } - if (conference != null) { - val state = conference.state - Log.i("[Conference] Found an existing conference: $conference in state $state") - if (state != Conference.State.TerminationPending && state != Conference.State.Terminated) { - initConference(conference) - if (state == Conference.State.Instantiated) { - conferenceCreationPending.value = true - } else if (state == Conference.State.Created) { - if (conferenceCreationPending.value == true) { - conferenceCreationPending.value = false - } - configureConference(conference) - } - } - } - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - conference.value?.removeListener(conferenceListener) - - conferenceParticipants.value.orEmpty().forEach(ConferenceParticipantData::destroy) - conferenceParticipantDevices.value.orEmpty().forEach( - ConferenceParticipantDeviceData::destroy - ) - activeSpeakerConferenceParticipantDevices.value.orEmpty().forEach( - ConferenceParticipantDeviceData::destroy - ) - - super.onCleared() - } - - fun pauseConference() { - Log.i("[Conference] Leaving conference temporarily") - conference.value?.leave() - } - - fun resumeConference() { - Log.i("[Conference] Entering conference again") - conference.value?.enter() - } - - fun toggleRecording() { - if (conference.value?.isRecording == true) { - Log.i("[Conference] Stopping conference recording") - conference.value?.stopRecording() - } else { - val path = LinphoneUtils.getRecordingFilePathForConference( - conference.value?.currentParams?.subject - ) - Log.i("[Conference] Starting recording in file $path") - conference.value?.startRecording(path) - } - isRecording.value = conference.value?.isRecording - } - - fun initConference(conference: Conference) { - conferenceExists.value = true - - this@ConferenceViewModel.conference.value = conference - conference.addListener(conferenceListener) - - isRecording.value = conference.isRecording - subject.value = LinphoneUtils.getConferenceSubject(conference) - - updateConferenceLayout(conference) - } - - fun configureConference(conference: Conference) { - updateParticipantsList(conference) - if (conferenceParticipants.value.orEmpty().isEmpty()) { - firstToJoinEvent.value = Event(true) - } - - updateParticipantsDevicesList(conference) - if (conferenceParticipantDevices.value.orEmpty().size == 2) { - secondParticipantJoinedEvent.value = Event(true) - } else if (conferenceParticipantDevices.value.orEmpty().size > 2) { - moreThanTwoParticipantsJoinedEvent.value = Event(true) - } - - isConferenceLocallyPaused.value = if (conference.call == null) false else !conference.isIn - isMeAdmin.value = conference.me.isAdmin - isVideoConference.value = conference.currentParams.isVideoEnabled && !corePreferences.disableVideo - subject.value = LinphoneUtils.getConferenceSubject(conference) - - updateConferenceLayout(conference) - } - - fun addCallsToConference() { - Log.i("[Conference] Trying to merge all calls into existing conference") - val conf = conference.value - conf ?: return - - for (call in coreContext.core.calls) { - if (call.conference == null) { - Log.i("[Conference] Adding call [$call] as participant for conference [$conf]") - conf.addParticipant(call) - } - } - if (!conf.isIn) { - Log.i("[Conference] Conference was paused, resuming it") - conf.enter() - } - } - - fun switchLayoutFromAudioOnlyToActiveSpeaker() { - if (conferenceDisplayMode.value == ConferenceDisplayMode.AUDIO_ONLY) { - Log.i( - "[Conference] Trying to switch from AUDIO_ONLY to ACTIVE_SPEAKER and toggle video ON" - ) - changeLayout(ConferenceDisplayMode.ACTIVE_SPEAKER, true) - waitForNextStreamsRunningToUpdateLayout = true - } else { - Log.w( - "[Conference] Can't switch from AUDIO_ONLY to ACTIVE_SPEAKER as current display mode isn't AUDIO_ONLY but ${conferenceDisplayMode.value}" - ) - } - } - - fun changeLayout(layout: ConferenceDisplayMode, forceSendingVideo: Boolean) { - Log.i("[Conference] Trying to change conference layout to $layout") - val conference = conference.value - if (conference != null) { - val call = conference.call - if (call != null) { - val params = call.core.createCallParams(call) - if (params == null) { - Log.e("[Conference] Failed to create call params from conference call!") - return - } - - params.isVideoEnabled = layout != ConferenceDisplayMode.AUDIO_ONLY - if (forceSendingVideo) { - Log.w("[Conference] Forcing video direction to SendRecv") - params.videoDirection = MediaDirection.SendRecv - } else { - if (conferenceDisplayMode.value == ConferenceDisplayMode.AUDIO_ONLY) { - // Previous layout was audio only, make sure video isn't sent without user consent when switching layout - params.videoDirection = MediaDirection.RecvOnly - } - Log.i("[Conference] Video direction is ${params.videoDirection}") - } - - params.conferenceVideoLayout = when (layout) { - ConferenceDisplayMode.GRID -> Conference.Layout.Grid - else -> Conference.Layout.ActiveSpeaker - } - call.update(params) - - conferenceDisplayMode.value = layout - val list = sortDevicesDataList(conferenceParticipantDevices.value.orEmpty()) - conferenceParticipantDevices.value = list - } else { - Log.e("[Conference] Failed to get call from conference!") - } - } else { - Log.e("[Conference] Conference is null in ConferenceViewModel") - } - } - - private fun updateConferenceLayout(conference: Conference) { - val call = conference.call - var videoDirection = MediaDirection.Inactive - - if (call == null) { - conferenceDisplayMode.value = ConferenceDisplayMode.AUDIO_ONLY - Log.w("[Conference] Call is null, assuming audio only layout for local conference") - } else { - val params = call.params - videoDirection = params.videoDirection - conferenceDisplayMode.value = if (!params.isVideoEnabled) { - ConferenceDisplayMode.AUDIO_ONLY - } else { - when (params.conferenceVideoLayout) { - Conference.Layout.Grid -> ConferenceDisplayMode.GRID - else -> ConferenceDisplayMode.ACTIVE_SPEAKER - } - } - } - - val list = sortDevicesDataList(conferenceParticipantDevices.value.orEmpty()) - conferenceParticipantDevices.value = list - - Log.i( - "[Conference] Current layout is [${conferenceDisplayMode.value}], video direction is [$videoDirection]" - ) - } - - private fun terminateConference(conference: Conference) { - conferenceExists.value = false - isVideoConference.value = false - - conference.removeListener(conferenceListener) - - conferenceParticipants.value.orEmpty().forEach(ConferenceParticipantData::destroy) - conferenceParticipantDevices.value.orEmpty().forEach( - ConferenceParticipantDeviceData::destroy - ) - activeSpeakerConferenceParticipantDevices.value.orEmpty().forEach( - ConferenceParticipantDeviceData::destroy - ) - - conferenceParticipants.value = arrayListOf() - conferenceParticipantDevices.value = arrayListOf() - } - - private fun updateParticipantsList(conference: Conference) { - conferenceParticipants.value.orEmpty().forEach(ConferenceParticipantData::destroy) - val participants = arrayListOf() - - val participantsList = conference.participantList - Log.i("[Conference] Conference has ${participantsList.size} participants") - for (participant in participantsList) { - val participantDevices = participant.devices - Log.i( - "[Conference] Participant found: ${participant.address.asStringUriOnly()} with ${participantDevices.size} device(s)" - ) - - val participantData = ConferenceParticipantData(conference, participant) - participants.add(participantData) - } - - conferenceParticipants.value = participants - } - - private fun updateParticipantsDevicesList(conference: Conference) { - conferenceParticipantDevices.value.orEmpty().forEach( - ConferenceParticipantDeviceData::destroy - ) - activeSpeakerConferenceParticipantDevices.value.orEmpty().forEach( - ConferenceParticipantDeviceData::destroy - ) - val devices = arrayListOf() - - val participantsList = conference.participantList - Log.i("[Conference] Conference has ${participantsList.size} participants") - - val activelySpeakingParticipantDevice = conference.activeSpeakerParticipantDevice - var foundActivelySpeakingParticipantDevice = false - speakingParticipantFound.value = false - speakingParticipantVideoEnabled.value = false - - val conferenceAddress = conference.conferenceAddress ?: return - val conferenceInfo = conference.core.findConferenceInformationFromUri( - conferenceAddress - ) - var allSpeaker = true - for (info in conferenceInfo?.participantInfos.orEmpty()) { - if (info.role == Participant.Role.Listener) { - allSpeaker = false - } - } - isBroadcast.value = !allSpeaker - if (!allSpeaker) { - Log.i( - "[Conference] Not all participants are speaker, considering it is a broadcast" - ) - } - - for (participant in participantsList) { - val participantDevices = participant.devices - Log.i( - "[Conference] Participant found: ${participant.address.asStringUriOnly()} with ${participantDevices.size} device(s)" - ) - - for (device in participantDevices) { - Log.i( - "[Conference] Participant device found: ${device.name} (${device.address.asStringUriOnly()})" - ) - - val info = conferenceInfo?.participantInfos?.find { - it.address.weakEqual(participant.address) - } - if (info != null) { - Log.i("[Conference] Participant role is [${info.role.name}]") - val listener = info.role == Participant.Role.Listener || info.role == Participant.Role.Unknown - if (listener) { - continue - } - } - - val deviceData = ConferenceParticipantDeviceData(device, false) - devices.add(deviceData) - - if (activelySpeakingParticipantDevice == device) { - Log.i( - "[Conference] Actively speaking participant device found: ${device.name} (${device.address.asStringUriOnly()})" - ) - speakingParticipant.value = deviceData - deviceData.isActiveSpeaker.value = true - foundActivelySpeakingParticipantDevice = true - speakingParticipantFound.value = true - speakingParticipantVideoEnabled.value = speakingParticipant.value?.isInConference?.value == true && speakingParticipant.value?.isSendingVideo?.value == true - } - } - } - - if (!foundActivelySpeakingParticipantDevice && devices.isNotEmpty()) { - Log.w( - "[Conference] Actively speaking participant device not found, using first participant device available" - ) - val deviceData = devices.first() - speakingParticipant.value = deviceData - deviceData.isActiveSpeaker.value = true - speakingParticipantFound.value = true - speakingParticipantVideoEnabled.value = speakingParticipant.value?.isInConference?.value == true && speakingParticipant.value?.isSendingVideo?.value == true - } - - for (device in conference.me.devices) { - Log.i( - "[Conference] Participant device for myself found: ${device.name} (${device.address.asStringUriOnly()})" - ) - - val info = conferenceInfo?.participantInfos?.find { - it.address.weakEqual(device.address) - } - if (info != null) { - Log.i("[Conference] Me role is [${info.role.name}]") - val listener = info.role == Participant.Role.Listener || info.role == Participant.Role.Unknown - isMeListenerOnly.value = listener - if (listener) { - continue - } - } - - val deviceData = ConferenceParticipantDeviceData(device, true) - devices.add(deviceData) - meParticipant.value = deviceData - } - - conferenceParticipantDevices.value = devices - twoOrMoreParticipants.value = devices.size >= 2 - moreThanTwoParticipants.value = devices.size > 2 - } - - private fun addParticipantDevice(conference: Conference, device: ParticipantDevice) { - val devices = arrayListOf() - devices.addAll(conferenceParticipantDevices.value.orEmpty()) - - val existingDevice = devices.find { - it.participantDevice.address.weakEqual(device.address) - } - if (existingDevice != null) { - Log.e( - "[Conference] Participant is already in devices list: ${device.name} (${device.address.asStringUriOnly()})" - ) - return - } - - Log.i( - "[Conference] New participant device found: ${device.name} (${device.address.asStringUriOnly()})" - ) - - val conferenceAddress = conference.conferenceAddress ?: return - val conferenceInfo = conference.core.findConferenceInformationFromUri( - conferenceAddress - ) - val info = conferenceInfo?.participantInfos?.find { - it.address.weakEqual(device.address) - } - if (info != null) { - Log.i("[Conference] New participant role is [${info.role.name}]") - val listener = - info.role == Participant.Role.Listener || info.role == Participant.Role.Unknown - if (listener) { - return - } - } - - val deviceData = ConferenceParticipantDeviceData(device, false) - devices.add(deviceData) - - val sortedDevices = sortDevicesDataList(devices) - - if (speakingParticipant.value == null || speakingParticipantFound.value == false) { - speakingParticipant.value = deviceData - deviceData.isActiveSpeaker.value = true - speakingParticipantFound.value = true - speakingParticipantVideoEnabled.value = speakingParticipant.value?.isInConference?.value == true && speakingParticipant.value?.isSendingVideo?.value == true - } - - conferenceParticipantDevices.value = sortedDevices - twoOrMoreParticipants.value = sortedDevices.size >= 2 - moreThanTwoParticipants.value = sortedDevices.size > 2 - } - - private fun removeParticipantDevice(device: ParticipantDevice) { - val devices = arrayListOf() - var removedDeviceWasActiveSpeaker = false - - for (participantDevice in conferenceParticipantDevices.value.orEmpty()) { - if (participantDevice.participantDevice.address.asStringUriOnly() != device.address.asStringUriOnly()) { - devices.add(participantDevice) - } else { - if (speakingParticipant.value == participantDevice) { - Log.w( - "[Conference] Removed participant device was the actively speaking participant device" - ) - removedDeviceWasActiveSpeaker = true - } - participantDevice.destroy() - } - } - - val devicesCount = devices.size - if (devicesCount == conferenceParticipantDevices.value.orEmpty().size) { - Log.e( - "[Conference] Failed to remove participant device: ${device.name} (${device.address.asStringUriOnly()})" - ) - } - - if (removedDeviceWasActiveSpeaker && devicesCount > 1) { - Log.w( - "[Conference] Updating actively speaking participant device using first one available" - ) - // Using second device as first is ourselves - val deviceData = devices[1] - speakingParticipant.value = deviceData - deviceData.isActiveSpeaker.value = true - speakingParticipantFound.value = true - speakingParticipantVideoEnabled.value = speakingParticipant.value?.isInConference?.value == true && speakingParticipant.value?.isSendingVideo?.value == true - } - - conferenceParticipantDevices.value = devices - twoOrMoreParticipants.value = devicesCount >= 2 - moreThanTwoParticipants.value = devicesCount > 2 - } - - private fun sortDevicesDataList(devices: List): ArrayList { - val sortedList = arrayListOf() - sortedList.addAll(devices) - - val meDeviceData = sortedList.find { - it.isMe - } - if (meDeviceData != null) { - val index = sortedList.indexOf(meDeviceData) - val expectedIndex = if (conferenceDisplayMode.value == ConferenceDisplayMode.ACTIVE_SPEAKER) { - 0 - } else { - sortedList.size - 1 - } - if (index != expectedIndex) { - Log.i( - "[Conference] Me device data is at index $index, moving it to index $expectedIndex" - ) - sortedList.removeAt(index) - sortedList.add(expectedIndex, meDeviceData) - } - } - - return sortedList - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt deleted file mode 100644 index fc8b58bb4..000000000 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/ControlsViewModel.kt +++ /dev/null @@ -1,595 +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 . - */ -package org.linphone.activities.voip.viewmodels - -import android.Manifest -import android.animation.ValueAnimator -import android.view.animation.LinearInterpolator -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.window.layout.FoldingFeature -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.* -import org.linphone.utils.Event - -class ControlsViewModel : ViewModel() { - val isSpeakerSelected = MutableLiveData() - - val isBluetoothHeadsetSelected = MutableLiveData() - - val audioRoutesSelected = MutableLiveData() - - val audioRoutesEnabled = MutableLiveData() - - val isVideoAvailable = MutableLiveData() - - val isVideoEnabled = MutableLiveData() - - val isSendingVideo = MutableLiveData() - - val isVideoUpdateInProgress = MutableLiveData() - - val isSwitchCameraAvailable = MutableLiveData() - - val isOutgoingEarlyMedia = MutableLiveData() - - val isIncomingEarlyMediaVideo = MutableLiveData() - - val isIncomingCallVideo = MutableLiveData() - - val showExtras = MutableLiveData() - - val fullScreenMode = MutableLiveData() - - val folded = MutableLiveData() - - val pipMode = MutableLiveData() - - val chatRoomCreationInProgress = MutableLiveData() - - val numpadVisible = MutableLiveData() - - val dtmfHistory = MutableLiveData() - - val callStatsVisible = MutableLiveData() - - val proximitySensorEnabled = MediatorLiveData() - - val forceDisableProximitySensor = MutableLiveData() - - val showTakeSnapshotButton = MutableLiveData() - - val attendedTransfer = MutableLiveData() - - val chatDisabled = MutableLiveData() - - val goToConferenceParticipantsListEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val goToChatEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val goToCallsListEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val goToConferenceLayoutSettingsEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val askPermissionEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val goToDialerEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val foldingState = MutableLiveData() - - val hideVideo = corePreferences.disableVideo - - private val nonEarpieceOutputAudioDevice = MutableLiveData() - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - Log.i("[Call Controls] State changed: $state") - isOutgoingEarlyMedia.value = state == Call.State.OutgoingEarlyMedia - isIncomingEarlyMediaVideo.value = state == Call.State.IncomingEarlyMedia && call.remoteParams?.isVideoEnabled == true - isIncomingCallVideo.value = call.remoteParams?.isVideoEnabled == true && coreContext.core.videoActivationPolicy.automaticallyAccept - attendedTransfer.value = core.callsNb > 1 - - if (state == Call.State.StreamsRunning) { - if (!call.currentParams.isVideoEnabled && fullScreenMode.value == true) { - fullScreenMode.value = false - } - isVideoUpdateInProgress.value = false - proximitySensorEnabled.value = shouldProximitySensorBeEnabled() - } else if (state == Call.State.PausedByRemote) { - fullScreenMode.value = false - } - - if (core.currentCall?.currentParams?.isVideoEnabled == true && !PermissionHelper.get().hasCameraPermission()) { - askPermissionEvent.value = Event(Manifest.permission.CAMERA) - } - - updateUI() - } - - override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) { - Log.i("[Call Controls] Audio device changed: ${audioDevice.deviceName}") - - nonEarpieceOutputAudioDevice.value = audioDevice.type != AudioDevice.Type.Earpiece - updateSpeakerState() - updateBluetoothHeadsetState() - } - - override fun onAudioDevicesListUpdated(core: Core) { - Log.i("[Call Controls] Audio devices list updated") - val wasBluetoothPreviouslyAvailable = audioRoutesEnabled.value == true - updateAudioRoutesState() - - if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) { - AudioRouteUtils.routeAudioToHeadset() - } else if (!wasBluetoothPreviouslyAvailable && corePreferences.routeAudioToBluetoothIfAvailable) { - // Only attempt to route audio to bluetooth automatically when bluetooth device is connected - if (AudioRouteUtils.isBluetoothAudioRouteAvailable()) { - AudioRouteUtils.routeAudioToBluetooth() - } - } - } - } - - val extraButtonsMenuTranslateY = MutableLiveData() - private val extraButtonsMenuAnimator: ValueAnimator by lazy { - ValueAnimator.ofFloat( - AppUtils.getDimension(R.dimen.voip_call_extra_buttons_translate_y), - 0f - ).apply { - addUpdateListener { - val value = it.animatedValue as Float - extraButtonsMenuTranslateY.value = value - } - duration = if (corePreferences.enableAnimations) 500 else 0 - } - } - - val audioRoutesMenuTranslateY = MutableLiveData() - private val audioRoutesMenuAnimator: ValueAnimator by lazy { - ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.voip_audio_routes_menu_translate_y), 0f).apply { - addUpdateListener { - val value = it.animatedValue as Float - audioRoutesMenuTranslateY.value = value - } - duration = if (corePreferences.enableAnimations) 500 else 0 - } - } - - val bouncyCounterTranslateY = MutableLiveData() - - private val bounceAnimator: ValueAnimator by lazy { - ValueAnimator.ofFloat(AppUtils.getDimension(R.dimen.voip_counter_bounce_offset), 0f).apply { - addUpdateListener { - val value = it.animatedValue as Float - bouncyCounterTranslateY.value = value - } - interpolator = LinearInterpolator() - duration = 250 - repeatMode = ValueAnimator.REVERSE - repeatCount = ValueAnimator.INFINITE - } - } - - init { - coreContext.core.addListener(listener) - - chatDisabled.value = corePreferences.disableChat - fullScreenMode.value = false - extraButtonsMenuTranslateY.value = AppUtils.getDimension( - R.dimen.voip_call_extra_buttons_translate_y - ) - audioRoutesMenuTranslateY.value = AppUtils.getDimension( - R.dimen.voip_audio_routes_menu_translate_y - ) - audioRoutesSelected.value = false - forceDisableProximitySensor.value = false - - nonEarpieceOutputAudioDevice.value = coreContext.core.outputAudioDevice?.type != AudioDevice.Type.Earpiece - proximitySensorEnabled.value = shouldProximitySensorBeEnabled() - proximitySensorEnabled.addSource(isVideoEnabled) { - proximitySensorEnabled.value = shouldProximitySensorBeEnabled() - } - proximitySensorEnabled.addSource(nonEarpieceOutputAudioDevice) { - proximitySensorEnabled.value = shouldProximitySensorBeEnabled() - } - proximitySensorEnabled.addSource(forceDisableProximitySensor) { - proximitySensorEnabled.value = shouldProximitySensorBeEnabled() - } - - val currentCall = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() - val state = currentCall?.state ?: Call.State.Idle - Log.i("[Call Controls] Current state is: $state") - isOutgoingEarlyMedia.value = state == Call.State.OutgoingEarlyMedia - isIncomingEarlyMediaVideo.value = state == Call.State.IncomingEarlyMedia && currentCall?.remoteParams?.isVideoEnabled == true - isIncomingCallVideo.value = currentCall?.remoteParams?.isVideoEnabled == true && coreContext.core.videoActivationPolicy.automaticallyAccept - - updateUI() - - if (corePreferences.enableAnimations) bounceAnimator.start() - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - - super.onCleared() - } - - fun hangUp() { - val core = coreContext.core - when { - core.currentCall != null -> core.currentCall?.terminate() - core.conference?.isIn == true -> core.terminateConference() - else -> core.terminateAllCalls() - } - } - - fun answer() { - val currentCall = coreContext.core.currentCall ?: coreContext.core.calls.find { - call -> - call.state == Call.State.IncomingReceived || call.state == Call.State.IncomingEarlyMedia - } - if (currentCall != null) { - coreContext.answerCall(currentCall) - } else { - Log.e("[Controls] Can't find any current call to answer") - } - } - - fun toggleSpeaker() { - if (AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed()) { - forceEarpieceAudioRoute() - } else { - forceSpeakerAudioRoute() - } - } - - fun toggleRoutesMenu() { - audioRoutesSelected.value = audioRoutesSelected.value != true - if (audioRoutesSelected.value == true) { - audioRoutesMenuAnimator.start() - } else { - audioRoutesMenuAnimator.reverse() - } - } - - fun forceEarpieceAudioRoute() { - if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) { - Log.i("[Call Controls] Headset found, route audio to it instead of earpiece") - AudioRouteUtils.routeAudioToHeadset() - } else { - AudioRouteUtils.routeAudioToEarpiece() - } - updateSpeakerState() - updateBluetoothHeadsetState() - } - - fun forceSpeakerAudioRoute() { - AudioRouteUtils.routeAudioToSpeaker() - updateSpeakerState() - updateBluetoothHeadsetState() - } - - fun forceBluetoothAudioRoute() { - AudioRouteUtils.routeAudioToBluetooth() - updateSpeakerState() - updateBluetoothHeadsetState() - } - - fun toggleVideo() { - if (!PermissionHelper.get().hasCameraPermission()) { - Log.w( - "[Call Controls] Camera permission isn't granted, asking it before toggling video" - ) - askPermissionEvent.value = Event(Manifest.permission.CAMERA) - return - } - - val core = coreContext.core - val currentCall = core.currentCall - if (currentCall != null) { - val state = currentCall.state - if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) { - Log.e("[Call Controls] Current call state is $state, aborting video toggle") - return - } - - isVideoUpdateInProgress.value = true - val params = core.createCallParams(currentCall) - if (currentCall.conference != null) { - if (params?.isVideoEnabled == false) { - params.isVideoEnabled = true - params.videoDirection = MediaDirection.SendRecv - } else { - if (params?.videoDirection == MediaDirection.SendRecv || params?.videoDirection == MediaDirection.SendOnly) { - params.videoDirection = MediaDirection.RecvOnly - } else { - params?.videoDirection = MediaDirection.SendRecv - } - } - } else { - params?.isVideoEnabled = params?.isVideoEnabled == false - Log.i( - "[Call Controls] Updating call with video enabled set to ${params?.isVideoEnabled}" - ) - } - currentCall.update(params) - } else { - Log.e("[Call Controls] Can't toggle video, no current call found!") - } - } - - fun switchCamera() { - coreContext.switchCamera() - } - - fun takeSnapshot() { - if (!PermissionHelper.get().hasWriteExternalStoragePermission()) { - askPermissionEvent.value = Event(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } else { - val currentCall = coreContext.core.currentCall - if (currentCall != null && currentCall.currentParams.isVideoEnabled) { - val fileName = System.currentTimeMillis().toString() + ".jpeg" - val fullPath = FileUtils.getFileStoragePath(fileName).absolutePath - Log.i("[Call Controls] Snapshot will be save under $fullPath") - currentCall.takeVideoSnapshot(fullPath) - } else { - Log.e("[Call Controls] Current call doesn't have video, can't take snapshot") - } - } - } - - fun showExtraButtons() { - extraButtonsMenuAnimator.start() - showExtras.value = true - } - - fun hideExtraButtons(skipAnimation: Boolean) { - // Animation must be skipped when called from Fragment's onPause() ! - if (skipAnimation) { - extraButtonsMenuTranslateY.value = AppUtils.getDimension( - R.dimen.voip_call_extra_buttons_translate_y - ) - } else { - extraButtonsMenuAnimator.reverse() - } - showExtras.value = false - chatRoomCreationInProgress.value = false - } - - fun toggleFullScreen() { - if (fullScreenMode.value == false && isVideoEnabled.value == false) return - fullScreenMode.value = fullScreenMode.value != true - } - - fun goToConferenceParticipantsList() { - goToConferenceParticipantsListEvent.value = Event(true) - } - - fun goToChat() { - chatRoomCreationInProgress.value = true - goToChatEvent.value = Event(true) - } - - fun showNumpad() { - hideExtraButtons(false) - numpadVisible.value = true - } - - fun hideNumpad() { - numpadVisible.value = false - } - - fun handleDtmfClick(key: Char) { - dtmfHistory.value = "${dtmfHistory.value.orEmpty()}$key" - coreContext.core.playDtmf(key, 1) - coreContext.core.currentCall?.sendDtmf(key) - } - - fun goToCallsList() { - goToCallsListEvent.value = Event(true) - } - - fun showCallStats(skipAnimation: Boolean = false) { - hideExtraButtons(skipAnimation) - callStatsVisible.value = true - } - - fun hideCallStats() { - callStatsVisible.value = false - } - - fun goToConferenceLayout() { - goToConferenceLayoutSettingsEvent.value = Event(true) - } - - fun transferCall() { - // In case there is more than 1 call, transfer will be attended instead of blind - if (coreContext.core.callsNb > 1) { - attendedTransfer() - } else { - goToDialerForCallTransfer() - } - } - - private fun attendedTransfer() { - val core = coreContext.core - val currentCall = core.currentCall - - if (currentCall == null) { - Log.e("[Call Controls] Can't do an attended transfer without a current call") - return - } - if (core.callsNb <= 1) { - Log.e("[Call Controls] Need at least two calls to do an attended transfer") - return - } - - val callToTransferTo = core.calls.findLast { - it.state == Call.State.Paused - } - if (callToTransferTo == null) { - Log.e( - "[Call Controls] Couldn't find a call in Paused state to transfer current call to" - ) - return - } - - Log.i( - "[Call Controls] Doing an attended transfer between active call [${currentCall.remoteAddress.asStringUriOnly()}] and paused call [${callToTransferTo.remoteAddress.asStringUriOnly()}]" - ) - val result = callToTransferTo.transferToAnother(currentCall) - if (result != 0) { - Log.e("[Call Controls] Attended transfer failed!") - } - } - - private fun goToDialerForCallTransfer() { - goToDialerEvent.value = Event(true) - } - - fun goToDialerForNewCall() { - goToDialerEvent.value = Event(false) - } - - private fun updateUI() { - updateVideoAvailable() - updateVideoEnabled() - updateSpeakerState() - updateBluetoothHeadsetState() - updateAudioRoutesState() - } - - private fun updateSpeakerState() { - isSpeakerSelected.value = AudioRouteUtils.isSpeakerAudioRouteCurrentlyUsed() - } - - private fun updateBluetoothHeadsetState() { - isBluetoothHeadsetSelected.value = AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed() - } - - private fun updateAudioRoutesState() { - val bluetoothDeviceAvailable = AudioRouteUtils.isBluetoothAudioRouteAvailable() - audioRoutesEnabled.value = bluetoothDeviceAvailable - - if (!bluetoothDeviceAvailable) { - audioRoutesSelected.value = false - } - } - - private fun updateVideoAvailable() { - val core = coreContext.core - val currentCall = core.currentCall - isVideoAvailable.value = (core.isVideoCaptureEnabled || core.isVideoPreviewEnabled) && - ((currentCall != null && !currentCall.mediaInProgress()) || core.conference?.isIn == true) - } - - private fun updateVideoEnabled() { - val currentCall = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() - val enabled = currentCall?.currentParams?.isVideoEnabled ?: false - // Prevent speaker to turn on each time a participant joins a video conference - val isConference = currentCall?.conference != null - if (enabled && !isConference && isVideoEnabled.value == false) { - Log.i("[Call Controls] Video is being turned on") - if (corePreferences.routeAudioToSpeakerWhenVideoIsEnabled) { - // Do not turn speaker on when video is enabled if headset or bluetooth is used - if (!AudioRouteUtils.isHeadsetAudioRouteAvailable() && - !AudioRouteUtils.isBluetoothAudioRouteCurrentlyUsed() - ) { - Log.i( - "[Call Controls] Video enabled and no wired headset not bluetooth in use, routing audio to speaker" - ) - AudioRouteUtils.routeAudioToSpeaker() - } - } - } - - isVideoEnabled.value = enabled - showTakeSnapshotButton.value = enabled && corePreferences.showScreenshotButton - val videoDirection = if (coreContext.core.currentCall?.conference != null) { - coreContext.core.currentCall?.currentParams?.videoDirection - } else { - coreContext.core.currentCall?.params?.videoDirection - } - val isVideoBeingSent = videoDirection == MediaDirection.SendRecv || videoDirection == MediaDirection.SendOnly - isSendingVideo.value = isVideoBeingSent - isSwitchCameraAvailable.value = enabled && coreContext.showSwitchCameraButton() && isVideoBeingSent - } - - private fun shouldProximitySensorBeEnabled(): Boolean { - val currentCall = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() - if (currentCall != null) { - when (val state = currentCall.state) { - Call.State.OutgoingEarlyMedia, Call.State.OutgoingProgress, Call.State.OutgoingRinging, Call.State.OutgoingInit -> { - Log.i( - "[Call Controls] Call is in outgoing state [$state], enabling proximity sensor" - ) - return true - } - Call.State.IncomingEarlyMedia, Call.State.IncomingReceived -> { - Log.i( - "[Call Controls] Call is in incoming state [$state], enabling proximity sensor" - ) - return true - } - else -> { } - } - } - - if (forceDisableProximitySensor.value == true) { - Log.i( - "[Call Controls] Forcing proximity sensor to be disabled (usually in incoming/outgoing call fragments)" - ) - } else if (isVideoEnabled.value == true) { - Log.i( - "[Call Controls] Active call current params says video is enabled, proximity sensor will be disabled" - ) - } else if (nonEarpieceOutputAudioDevice.value == true) { - Log.i( - "[Call Controls] Current audio route is not earpiece, proximity sensor will be disabled" - ) - } - - return forceDisableProximitySensor.value == false && - !(isVideoEnabled.value ?: false) && - !(nonEarpieceOutputAudioDevice.value ?: false) - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/StatisticsListViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/StatisticsListViewModel.kt deleted file mode 100644 index 2f7fbadbe..000000000 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/StatisticsListViewModel.kt +++ /dev/null @@ -1,91 +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 . - */ -package org.linphone.activities.voip.viewmodels - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.voip.data.CallStatisticsData -import org.linphone.core.Call -import org.linphone.core.Core -import org.linphone.core.CoreListenerStub - -class StatisticsListViewModel : ViewModel() { - val callStatsList = MutableLiveData>() - - private var enabled = false - - private val listener = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - if (state == Call.State.End || state == Call.State.Error || state == Call.State.Connected) { - computeCallsList() - } - } - } - - init { - coreContext.core.addListener(listener) - - computeCallsList() - } - - fun enable() { - enabled = true - for (stat in callStatsList.value.orEmpty()) { - stat.enable() - } - } - - fun disable() { - enabled = false - for (stat in callStatsList.value.orEmpty()) { - stat.disable() - } - } - - override fun onCleared() { - callStatsList.value.orEmpty().forEach(CallStatisticsData::destroy) - coreContext.core.removeListener(listener) - - super.onCleared() - } - - private fun computeCallsList() { - callStatsList.value.orEmpty().forEach(CallStatisticsData::destroy) - - val list = arrayListOf() - for (call in coreContext.core.calls) { - if (call.state != Call.State.End && call.state != Call.State.Released && call.state != Call.State.Error) { - val data = CallStatisticsData(call) - list.add(data) - if (enabled) { - data.enable() - } - } - } - - callStatsList.value = list - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt b/app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt deleted file mode 100644 index ec2d25fbd..000000000 --- a/app/src/main/java/org/linphone/activities/voip/viewmodels/StatusViewModel.kt +++ /dev/null @@ -1,175 +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 . - */ -package org.linphone.activities.voip.viewmodels - -import androidx.lifecycle.MutableLiveData -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.main.viewmodels.StatusViewModel -import org.linphone.core.* -import org.linphone.utils.Event - -class StatusViewModel : StatusViewModel() { - val callQualityIcon = MutableLiveData() - val callQualityContentDescription = MutableLiveData() - - val encryptionIcon = MutableLiveData() - val encryptionContentDescription = MutableLiveData() - val encryptionIconVisible = MutableLiveData() - - val showZrtpDialogEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - val showCallStatsEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - var previouslyDeclineToken = false - - private val listener = object : CoreListenerStub() { - override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) { - updateCallQualityIcon() - } - - override fun onCallEncryptionChanged( - core: Core, - call: Call, - on: Boolean, - authenticationToken: String? - ) { - updateEncryptionInfo(call) - // Check if we just declined a previously validated token - // In that case, don't show the ZRTP dialog again - if (!previouslyDeclineToken) { - previouslyDeclineToken = false - showZrtpDialog(call) - } - } - - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - if (call == core.currentCall) { - updateEncryptionInfo(call) - } - } - } - - init { - coreContext.core.addListener(listener) - - updateCallQualityIcon() - - val currentCall = coreContext.core.currentCall - if (currentCall != null) { - updateEncryptionInfo(currentCall) - showZrtpDialog(currentCall) - } - } - - override fun onCleared() { - coreContext.core.removeListener(listener) - - super.onCleared() - } - - fun showZrtpDialog() { - val currentCall = coreContext.core.currentCall - if (currentCall != null) { - showZrtpDialog(currentCall, force = true) - } - } - - fun showCallStats() { - showCallStatsEvent.value = Event(true) - } - - fun updateEncryptionInfo(call: Call) { - if (call.dir == Call.Dir.Incoming && call.state == Call.State.IncomingReceived && call.core.isMediaEncryptionMandatory) { - // If the incoming call view is displayed while encryption is mandatory, - // we can safely show the security_ok icon - encryptionIcon.value = R.drawable.security_ok - encryptionIconVisible.value = true - encryptionContentDescription.value = R.string.content_description_call_secured - return - } - if (call.state == Call.State.End || call.state == Call.State.Released) { - return - } - - when (call.currentParams.mediaEncryption ?: MediaEncryption.None) { - MediaEncryption.SRTP, MediaEncryption.DTLS -> { - encryptionIcon.value = R.drawable.security_ok - encryptionIconVisible.value = true - encryptionContentDescription.value = R.string.content_description_call_secured - } - MediaEncryption.ZRTP -> { - encryptionIcon.value = when (call.authenticationTokenVerified || call.authenticationToken == null) { - true -> R.drawable.security_ok - else -> R.drawable.security_pending - } - encryptionContentDescription.value = when (call.authenticationTokenVerified || call.authenticationToken == null) { - true -> R.string.content_description_call_secured - else -> R.string.content_description_call_security_pending - } - encryptionIconVisible.value = true - } - MediaEncryption.None -> { - encryptionIcon.value = R.drawable.security_ko - // Do not show unsecure icon if user doesn't want to do call encryption - encryptionIconVisible.value = call.core.mediaEncryption != MediaEncryption.None - encryptionContentDescription.value = R.string.content_description_call_not_secured - } - } - } - - private fun showZrtpDialog(call: Call, force: Boolean = false) { - if ( - call.currentParams.mediaEncryption == MediaEncryption.ZRTP && - call.authenticationToken != null && - (!call.authenticationTokenVerified || force) - ) { - showZrtpDialogEvent.value = Event(call) - } - } - - private fun updateCallQualityIcon() { - val call = coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() - val quality = call?.currentQuality ?: 0f - callQualityIcon.value = when { - quality >= 4 -> R.drawable.call_quality_indicator_4 - quality >= 3 -> R.drawable.call_quality_indicator_3 - quality >= 2 -> R.drawable.call_quality_indicator_2 - quality >= 1 -> R.drawable.call_quality_indicator_1 - else -> R.drawable.call_quality_indicator_0 - } - callQualityContentDescription.value = when { - quality >= 4 -> R.string.content_description_call_quality_4 - quality >= 3 -> R.string.content_description_call_quality_3 - quality >= 2 -> R.string.content_description_call_quality_2 - quality >= 1 -> R.string.content_description_call_quality_1 - else -> R.string.content_description_call_quality_0 - } - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/views/GridBoxLayout.kt b/app/src/main/java/org/linphone/activities/voip/views/GridBoxLayout.kt deleted file mode 100644 index 37a57c1dc..000000000 --- a/app/src/main/java/org/linphone/activities/voip/views/GridBoxLayout.kt +++ /dev/null @@ -1,121 +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 . - */ -package org.linphone.activities.voip.views - -import android.annotation.SuppressLint -import android.content.Context -import android.util.AttributeSet -import android.widget.GridLayout -import androidx.core.view.children -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.core.tools.Log - -class GridBoxLayout : GridLayout { - companion object { - private val placementMatrix = arrayOf( - intArrayOf(1, 2, 3, 4, 5, 6), - intArrayOf(1, 1, 2, 2, 3, 3), - intArrayOf(1, 1, 1, 2, 2, 2), - intArrayOf(1, 1, 1, 1, 2, 2), - intArrayOf(1, 1, 1, 1, 1, 2), - intArrayOf(1, 1, 1, 1, 1, 1) - ) - } - - constructor(context: Context) : this(context, null) - constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) - - var centerContent: Boolean = false - private var previousChildCount = 0 - private var previousCellSize = 0 - - @SuppressLint("DrawAllocation") - override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { - if (childCount == 0 || (!changed && previousChildCount == childCount)) { - super.onLayout(changed, left, top, right, bottom) - // To prevent display issue the first time conference is locally paused - children.forEach { child -> - child.post { - child.layoutParams.width = previousCellSize - child.layoutParams.height = previousCellSize - child.requestLayout() - } - } - return - } - - // To prevent java.lang.IllegalArgumentException: columnCount must be greater than or equal - // to the maximum of all grid indices (and spans) defined in the LayoutParams of each child. - children.forEach { child -> - child.layoutParams = LayoutParams() - } - - val maxChild = placementMatrix[0].size - if (childCount > maxChild) { - val maxMosaicParticipants = corePreferences.maxConferenceParticipantsForMosaicLayout - Log.e( - "[GridBoxLayout] $childCount children but placementMatrix only knows how to display $maxChild (max allowed participants for grid layout in settings is $maxMosaicParticipants)" - ) - return - } - - val availableSize = Pair(right - left, bottom - top) - var cellSize = 0 - for (index in 1..childCount) { - val neededColumns = placementMatrix[index - 1][childCount - 1] - val candidateWidth = 1 * availableSize.first / neededColumns - val candidateHeight = 1 * availableSize.second / index - val candidateSize = if (candidateWidth < candidateHeight) candidateWidth else candidateHeight - if (candidateSize > cellSize) { - columnCount = neededColumns - rowCount = index - cellSize = candidateSize - } - } - previousCellSize = cellSize - previousChildCount = childCount - - super.onLayout(changed, left, top, right, bottom) - children.forEach { child -> - child.layoutParams.width = cellSize - child.layoutParams.height = cellSize - child.post { - child.requestLayout() - } - } - - if (centerContent) { - setPadding( - (availableSize.first - (columnCount * cellSize)) / 2, - (availableSize.second - (rowCount * cellSize)) / 2, - (availableSize.first - (columnCount * cellSize)) / 2, - (availableSize.second - (rowCount * cellSize)) / 2 - ) - } - Log.d( - "[GridBoxLayout] cellsize=$cellSize columns=$columnCount rows=$rowCount availablesize=$availableSize" - ) - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/views/RoundCornersTextureView.kt b/app/src/main/java/org/linphone/activities/voip/views/RoundCornersTextureView.kt deleted file mode 100644 index 80a4279a6..000000000 --- a/app/src/main/java/org/linphone/activities/voip/views/RoundCornersTextureView.kt +++ /dev/null @@ -1,117 +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 . - */ -package org.linphone.activities.voip.views - -import android.content.Context -import android.graphics.Outline -import android.graphics.Rect -import android.util.AttributeSet -import android.view.View -import android.view.ViewOutlineProvider -import org.linphone.R -import org.linphone.mediastream.video.capture.CaptureTextureView - -class RoundCornersTextureView : CaptureTextureView { - private var mRadius: Float = 0f - - constructor(context: Context) : super(context) { - mAlignTopRight = true - mDisplayMode = DisplayMode.BLACK_BARS - setRoundCorners() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - readAttributes(attrs) - setRoundCorners() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - readAttributes(attrs) - setRoundCorners() - } - - private fun readAttributes(attrs: AttributeSet) { - context.theme.obtainStyledAttributes( - attrs, - R.styleable.RoundCornersTextureView, - 0, - 0 - ).apply { - try { - mAlignTopRight = getBoolean(R.styleable.RoundCornersTextureView_alignTopRight, true) - val mode = getInteger( - R.styleable.RoundCornersTextureView_displayMode, - DisplayMode.BLACK_BARS.ordinal - ) - mDisplayMode = when (mode) { - 1 -> DisplayMode.OCCUPY_ALL_SPACE - 2 -> DisplayMode.HYBRID - else -> DisplayMode.BLACK_BARS - } - mRadius = getFloat( - R.styleable.RoundCornersTextureView_radius, - context.resources.getDimension(R.dimen.voip_round_corners_texture_view_radius) - ) - } finally { - recycle() - } - } - } - - private fun setRoundCorners() { - outlineProvider = object : ViewOutlineProvider() { - override fun getOutline(view: View, outline: Outline) { - val rect = if (previewRectF != null && - actualDisplayMode == DisplayMode.BLACK_BARS && - mAlignTopRight - ) { - Rect( - previewRectF.left.toInt(), - previewRectF.top.toInt(), - previewRectF.right.toInt(), - previewRectF.bottom.toInt() - ) - } else { - Rect( - 0, - 0, - width, - height - ) - } - outline.setRoundRect(rect, mRadius) - } - } - clipToOutline = true - } - - override fun setAspectRatio(width: Int, height: Int) { - super.setAspectRatio(width, height) - - val previewSize = previewVideoSize - if (previewSize.width > 0 && previewSize.height > 0) { - setRoundCorners() - } - } -} diff --git a/app/src/main/java/org/linphone/activities/voip/views/ScrollDotsView.kt b/app/src/main/java/org/linphone/activities/voip/views/ScrollDotsView.kt deleted file mode 100644 index e0498b948..000000000 --- a/app/src/main/java/org/linphone/activities/voip/views/ScrollDotsView.kt +++ /dev/null @@ -1,261 +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 . - */ -package org.linphone.activities.voip.views - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.Paint -import android.graphics.Rect -import android.util.AttributeSet -import android.view.View -import android.view.View.OnScrollChangeListener -import android.widget.FrameLayout -import android.widget.HorizontalScrollView -import android.widget.ScrollView -import kotlin.math.ceil -import kotlin.math.roundToInt -import org.linphone.R -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils - -class ScrollDotsView : View { - private var count = 2 - private var itemCount = 0 - private var selected = 0 - - private var radius: Float = 5f - private var margin: Float = 2f - - private var screenWidth: Float = 0f - private var itemWidth: Float = 0f - private var screenHeight: Float = 0f - private var itemHeight: Float = 0f - - private lateinit var dotPaint: Paint - private lateinit var selectedDotPaint: Paint - - private var scrollViewRef = 0 - private lateinit var scrollView: FrameLayout - private var isHorizontal = false - - private val scrollListener = OnScrollChangeListener { v, scrollX, scrollY, _, _ -> - if (isHorizontal) { - if (v !is HorizontalScrollView) { - Log.e("[Scoll Dots] ScrollView reference isn't a HorizontalScrollView!") - return@OnScrollChangeListener - } - - val childWidth: Int = v.getChildAt(0).measuredWidth - val scrollViewWidth = v.measuredWidth - val scrollableX = childWidth - scrollViewWidth - - if (scrollableX > 0) { - val percent = (scrollX.toFloat() * 100 / scrollableX).toDouble() - if (count > 1) { - val selectedDot = percent / (100 / (count - 1)) - val dot = selectedDot.roundToInt() - if (dot != selected) { - setSelectedDot(dot) - } - } - } - } else { - if (v !is ScrollView) { - Log.e("[Scoll Dots] ScrollView reference isn't a ScrollView!") - return@OnScrollChangeListener - } - - val childHeight: Int = v.getChildAt(0).measuredHeight - val scrollViewHeight = v.measuredHeight - val scrollableY = childHeight - scrollViewHeight - - if (scrollableY > 0) { - val percent = (scrollY.toFloat() * 100 / scrollableY).toDouble() - if (count > 1) { - val selectedDot = percent / (100 / (count - 1)) - val dot = selectedDot.roundToInt() - if (dot != selected) { - setSelectedDot(dot) - } - } - } - } - } - - constructor(context: Context) : super(context) { init(context) } - - constructor(context: Context, attrs: AttributeSet) : this(context, attrs, 0) - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init(context) - - context.theme.obtainStyledAttributes( - attrs, - R.styleable.ScrollDot, - defStyleAttr, - 0 - ).apply { - try { - radius = getDimension(R.styleable.ScrollDot_dotRadius, 5f) - - count = getInt(R.styleable.ScrollDot_dotCount, 1) - - val color = getColor( - R.styleable.ScrollDot_dotColor, - context.resources.getColor(R.color.voip_gray_dots) - ) - dotPaint.color = color - val selectedColor = getColor( - R.styleable.ScrollDot_selectedDotColor, - context.resources.getColor(R.color.voip_dark_gray) - ) - selectedDotPaint.color = selectedColor - - selected = getInt(R.styleable.ScrollDot_selectedDot, 1) - - scrollViewRef = getResourceId(R.styleable.ScrollDot_scrollView, 0) - - invalidate() - } catch (e: Exception) { - Log.e("[Scroll Dots] $e") - } finally { - recycle() - } - } - } - - fun init(context: Context) { - radius = AppUtils.dpToPixels(context, 5f) - margin = AppUtils.dpToPixels(context, 5f) - - dotPaint = Paint() - dotPaint.color = Color.parseColor("#D8D8D8") - selectedDotPaint = Paint() - selectedDotPaint.color = Color.parseColor("#4B5964") - - val screenRect = Rect() - getWindowVisibleDisplayFrame(screenRect) - screenWidth = screenRect.width().toFloat() - screenHeight = screenRect.height().toFloat() - - val marginBetweenItems = context.resources.getDimension( - R.dimen.voip_active_speaker_miniature_margin - ) - itemWidth = context.resources.getDimension(R.dimen.voip_active_speaker_miniature_size) + marginBetweenItems - itemHeight = context.resources.getDimension(R.dimen.voip_active_speaker_miniature_size) + marginBetweenItems - - Log.d( - "[Scroll Dots] Screen size is $screenWidth/$screenHeight and item size is $itemWidth/$itemHeight" - ) - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - - for (i in 0 until count) { - if (i == selected) { - val position = (i + 1) * margin + (i * 2 + 1) * radius - canvas.drawCircle( - if (isHorizontal) position else radius, - if (isHorizontal) radius else position, - radius, - selectedDotPaint - ) - } else { - val position = (i + 1) * margin + (i * 2 + 1) * radius - canvas.drawCircle( - if (isHorizontal) position else radius, - if (isHorizontal) radius else position, - radius, - dotPaint - ) - } - } - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - - checkOrientation() - - if (isHorizontal) { - val width = ((radius * 2 + margin) * count + margin).toInt() - val height: Int = (radius * 2).toInt() - setMeasuredDimension(width, height) - } else { - val height = ((radius * 2 + margin) * count + margin).toInt() - val width: Int = (radius * 2).toInt() - setMeasuredDimension(width, height) - } - } - - private fun checkOrientation() { - if (scrollViewRef > 0) { - try { - scrollView = (parent as View).findViewById(scrollViewRef) - scrollView.setOnScrollChangeListener(scrollListener) - Log.d("[Scroll Dots] ScrollView scroll listener set") - isHorizontal = scrollView is HorizontalScrollView - Log.d("[Scroll Dots] ScrollView is horizontal ? $isHorizontal") - requestLayout() - setItemCount(itemCount) - } catch (e: Exception) { - Log.e("[Scroll Dots] Failed to find ScrollView from id $scrollViewRef: $e") - } - } else { - Log.e("[Scroll Dots] No ScrollView reference given") - } - } - - private fun setDotCount(count: Int) { - this.count = count - requestLayout() - invalidate() - } - - fun setItemCount(items: Int) { - itemCount = items - if (isHorizontal) { - val itemsPerScreen = (screenWidth / itemWidth) - val dots = ceil(items.toDouble() / itemsPerScreen).toInt() - Log.d( - "[Scroll Dots] Calculated $count for $items items ($itemsPerScreen items fit in screen width), given that screen width is $screenWidth and item width is $itemWidth" - ) - setDotCount(dots) - } else { - val itemsPerScreen = (screenHeight / itemHeight) - val dots = ceil(items.toDouble() / itemsPerScreen).toInt() - Log.d( - "[Scroll Dots] Calculated $count for $items items ($itemsPerScreen items fit in screen height), given that screen height is $screenHeight and item height is $itemHeight" - ) - setDotCount(dots) - } - } - - fun setSelectedDot(index: Int) { - selected = index - invalidate() - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt deleted file mode 100644 index 25e399413..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api23Compatibility.kt +++ /dev/null @@ -1,286 +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 . - */ -package org.linphone.compatibility - -import android.Manifest -import android.annotation.SuppressLint -import android.app.Activity -import android.app.Notification -import android.app.PendingIntent -import android.app.Service -import android.bluetooth.BluetoothAdapter -import android.content.ContentValues -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.net.Uri -import android.os.Build -import android.os.Environment -import android.os.Vibrator -import android.provider.MediaStore -import android.provider.Settings -import android.view.View -import android.view.Window -import android.view.WindowManager -import android.view.inputmethod.EditorInfo -import androidx.fragment.app.Fragment -import org.linphone.R -import org.linphone.core.Content -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils -import org.linphone.utils.FileUtils -import org.linphone.utils.PermissionHelper - -class Api23Compatibility { - companion object { - fun hasPermission(context: Context, permission: String): Boolean { - return context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED - } - - fun requestReadPhoneStatePermission(fragment: Fragment, code: Int) { - fragment.requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), code) - } - - fun canDrawOverlay(context: Context): Boolean { - return Settings.canDrawOverlays(context) - } - - fun getBitmapFromUri(context: Context, uri: Uri): Bitmap { - return MediaStore.Images.Media.getBitmap(context.contentResolver, uri) - } - - @SuppressLint("MissingPermission") - fun eventVibration(vibrator: Vibrator) { - val pattern = longArrayOf(0, 100, 100) - vibrator.vibrate(pattern, -1) - } - - @SuppressLint("MissingPermission") - fun getDeviceName(context: Context): String { - val adapter = BluetoothAdapter.getDefaultAdapter() - var name = adapter?.name - if (name == null) { - name = Settings.Secure.getString( - context.contentResolver, - "bluetooth_name" - ) - } - if (name == null) { - name = Build.MANUFACTURER + " " + Build.MODEL - } - return name - } - - fun setShowWhenLocked(activity: Activity, enable: Boolean) { - if (enable) { - activity.window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) - } else { - activity.window.clearFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) - } - } - - fun setTurnScreenOn(activity: Activity, enable: Boolean) { - if (enable) { - activity.window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) - } else { - activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON) - } - } - - fun requestDismissKeyguard(activity: Activity) { - activity.window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD) - } - - fun startForegroundService(context: Context, intent: Intent) { - context.startService(intent) - } - - fun startForegroundService(service: Service, notifId: Int, notif: Notification?) { - service.startForeground(notifId, notif) - } - - fun hideAndroidSystemUI(hide: Boolean, window: Window) { - val decorView = window.decorView - val uiOptions = if (hide) { - View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - } else { - View.SYSTEM_UI_FLAG_VISIBLE - } - decorView.systemUiVisibility = uiOptions - - if (hide) { - window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) - } else { - window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION) - } - } - - fun getUpdateCurrentPendingIntentFlag(): Int { - return PendingIntent.FLAG_UPDATE_CURRENT - } - - fun getImeFlagsForSecureChatRoom(): Int { - return EditorInfo.IME_NULL - } - - suspend fun addImageToMediaStore(context: Context, content: Content): Boolean { - return addContentToMediaStore(context, content, isImage = true) - } - - suspend fun addVideoToMediaStore(context: Context, content: Content): Boolean { - return addContentToMediaStore(context, content, isVideo = true) - } - - suspend fun addAudioToMediaStore(context: Context, content: Content): Boolean { - return addContentToMediaStore(context, content, isAudio = true) - } - - private suspend fun addContentToMediaStore( - context: Context, - content: Content, - isImage: Boolean = false, - isVideo: Boolean = false, - isAudio: Boolean = false - ): Boolean { - if (!PermissionHelper.get().hasWriteExternalStoragePermission()) { - Log.e("[Media Store] Write external storage permission denied") - return false - } - - val isContentEncrypted = content.isFileEncrypted - val filePath = if (content.isFileEncrypted) { - val plainFilePath = content.exportPlainFile().orEmpty() - Log.i( - "[Media Store] [VFS] Content is encrypted, plain file path is: $plainFilePath" - ) - plainFilePath - } else { - content.filePath - } - - if (filePath.isNullOrEmpty()) { - Log.e("[Media Store] Content doesn't have a file path!") - return false - } - - val appName = AppUtils.getString(R.string.app_name) - val directory = when { - isImage -> Environment.DIRECTORY_PICTURES - isVideo -> Environment.DIRECTORY_MOVIES - isAudio -> Environment.DIRECTORY_MUSIC - else -> Environment.DIRECTORY_DOWNLOADS - } - val relativePath = "$directory/$appName" - val fileName = content.name - val mime = "${content.type}/${content.subtype}" - val type = when { - isImage -> "image" - isVideo -> "video" - isAudio -> "audio" - else -> "unexpected" - } - Log.i( - "[Media Store] Adding $type [$filePath] to Media Store with name [$fileName] and MIME [$mime], asking to be stored in: $relativePath" - ) - - val mediaStoreFilePath = when { - isImage -> { - val values = ContentValues().apply { - put(MediaStore.Images.Media.DISPLAY_NAME, fileName) - put(MediaStore.Images.Media.MIME_TYPE, mime) - } - val collection = MediaStore.Images.Media.getContentUri("external") - addContentValuesToCollection(context, filePath, collection, values) - } - isVideo -> { - val values = ContentValues().apply { - put(MediaStore.Video.Media.TITLE, fileName) - put(MediaStore.Video.Media.DISPLAY_NAME, fileName) - put(MediaStore.Video.Media.MIME_TYPE, mime) - } - val collection = MediaStore.Video.Media.getContentUri("external") - addContentValuesToCollection(context, filePath, collection, values) - } - isAudio -> { - val values = ContentValues().apply { - put(MediaStore.Audio.Media.TITLE, fileName) - put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) - put(MediaStore.Audio.Media.MIME_TYPE, mime) - } - val collection = MediaStore.Audio.Media.getContentUri("external") - addContentValuesToCollection(context, filePath, collection, values) - } - else -> "" - } - - if (isContentEncrypted) { - Log.w("[Media Store] Content was encrypted, delete plain version: $filePath") - FileUtils.deleteFile(filePath) - } - - if (mediaStoreFilePath.isNotEmpty()) { - Log.i("[Media Store] Exported file path is: $mediaStoreFilePath") - content.userData = mediaStoreFilePath - return true - } - - return false - } - - private suspend fun addContentValuesToCollection( - context: Context, - filePath: String, - collection: Uri, - values: ContentValues - ): String { - try { - val fileUri = context.contentResolver.insert(collection, values) - if (fileUri == null) { - Log.e("[Media Store] Failed to get a URI to where store the file, aborting") - return "" - } - - context.contentResolver.openOutputStream(fileUri).use { out -> - if (FileUtils.copyFileTo(filePath, out)) { - return fileUri.toString() - } - } - } catch (e: Exception) { - Log.e("[Media Store] Exception: $e") - } - return "" - } - - fun requestReadExternalStorageAndCameraPermissions(fragment: Fragment, code: Int) { - fragment.requestPermissions( - arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.CAMERA - ), - code - ) - } - - fun hasReadExternalStoragePermission(context: Context): Boolean { - return Compatibility.hasPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE) - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt deleted file mode 100644 index 39111541f..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api25Compatibility.kt +++ /dev/null @@ -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 . - */ -package org.linphone.compatibility - -import android.annotation.SuppressLint -import android.annotation.TargetApi -import android.app.Activity -import android.bluetooth.BluetoothAdapter -import android.content.Context -import android.os.Build -import android.provider.Settings - -@TargetApi(25) -class Api25Compatibility { - companion object { - @SuppressLint("MissingPermission") - fun getDeviceName(context: Context): String { - var name = Settings.Global.getString( - context.contentResolver, - Settings.Global.DEVICE_NAME - ) - if (name == null) { - val adapter = BluetoothAdapter.getDefaultAdapter() - name = adapter?.name - } - if (name == null) { - name = Settings.Secure.getString( - context.contentResolver, - "bluetooth_name" - ) - } - if (name == null) { - name = Build.MANUFACTURER + " " + Build.MODEL - } - return name - } - - fun isInPictureInPictureMode(activity: Activity): Boolean { - return activity.isInPictureInPictureMode - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt deleted file mode 100644 index 95dcb07af..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api26Compatibility.kt +++ /dev/null @@ -1,415 +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 . - */ -package org.linphone.compatibility - -import android.Manifest -import android.annotation.SuppressLint -import android.annotation.TargetApi -import android.app.* -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.media.AudioAttributes -import android.os.VibrationEffect -import android.os.Vibrator -import android.telecom.CallAudioState -import android.view.WindowManager -import android.view.inputmethod.EditorInfo -import android.widget.RemoteViews -import androidx.annotation.StringRes -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.Person -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.IconCompat -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.contact.getThumbnailUri -import org.linphone.core.Call -import org.linphone.core.Friend -import org.linphone.core.tools.Log -import org.linphone.notifications.Notifiable -import org.linphone.notifications.NotificationsManager -import org.linphone.telecom.NativeCallWrapper -import org.linphone.utils.ImageUtils -import org.linphone.utils.LinphoneUtils - -@TargetApi(26) -class Api26Compatibility { - companion object { - fun enterPipMode(activity: Activity, conference: Boolean) { - val supportsPip = activity.packageManager - .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) - Log.i("[Call] Is PiP supported: $supportsPip") - if (supportsPip) { - // Force portrait layout if in conference, otherwise for landscape - // Our layouts behave better in these orientation - val params = PictureInPictureParams.Builder() - .setAspectRatio(Compatibility.getPipRatio(activity, conference, !conference)) - .build() - try { - if (!activity.enterPictureInPictureMode(params)) { - Log.e("[Call] Failed to enter PiP mode") - } else { - Log.i( - "[Call] Entering PiP mode with ${if (conference) "portrait" else "landscape"} aspect ratio" - ) - } - } catch (e: Exception) { - Log.e("[Call] Can't build PiP params: $e") - } - } - } - - fun createServiceChannel(context: Context, notificationManager: NotificationManagerCompat) { - // Create service notification channel - val id = context.getString(R.string.notification_channel_service_id) - val name = context.getString(R.string.notification_channel_service_name) - val description = context.getString(R.string.notification_channel_service_name) - val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW) - channel.description = description - channel.enableVibration(false) - channel.enableLights(false) - channel.setShowBadge(false) - notificationManager.createNotificationChannel(channel) - } - - fun createMissedCallChannel( - context: Context, - notificationManager: NotificationManagerCompat - ) { - val id = context.getString(R.string.notification_channel_missed_call_id) - val name = context.getString(R.string.notification_channel_missed_call_name) - val description = context.getString(R.string.notification_channel_missed_call_name) - val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_LOW) - channel.description = description - channel.lightColor = context.getColor(R.color.notification_led_color) - channel.enableVibration(true) - channel.enableLights(true) - channel.setShowBadge(true) - notificationManager.createNotificationChannel(channel) - } - - fun createIncomingCallChannel( - context: Context, - notificationManager: NotificationManagerCompat - ) { - // Create incoming calls notification channel - val id = context.getString(R.string.notification_channel_incoming_call_id) - val name = context.getString(R.string.notification_channel_incoming_call_name) - val description = context.getString(R.string.notification_channel_incoming_call_name) - val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH) - channel.description = description - channel.lightColor = context.getColor(R.color.notification_led_color) - channel.enableVibration(true) - channel.enableLights(true) - channel.setShowBadge(true) - notificationManager.createNotificationChannel(channel) - } - - fun createMessageChannel( - context: Context, - notificationManager: NotificationManagerCompat - ) { - // Create messages notification channel - val id = context.getString(R.string.notification_channel_chat_id) - val name = context.getString(R.string.notification_channel_chat_name) - val description = context.getString(R.string.notification_channel_chat_name) - val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH) - channel.description = description - channel.lightColor = context.getColor(R.color.notification_led_color) - channel.enableLights(true) - channel.enableVibration(true) - channel.setShowBadge(true) - notificationManager.createNotificationChannel(channel) - } - - fun getChannelImportance( - notificationManager: NotificationManagerCompat, - channelId: String - ): Int { - val channel = notificationManager.getNotificationChannel(channelId) - return channel?.importance ?: NotificationManagerCompat.IMPORTANCE_NONE - } - - fun getOverlayType(): Int { - return WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY - } - - fun createIncomingCallNotification( - context: Context, - call: Call, - notifiable: Notifiable, - pendingIntent: PendingIntent, - notificationsManager: NotificationsManager - ): Notification { - val contact: Friend? - val roundPicture: Bitmap? - val displayName: String - val address: String - val info: String - - val remoteContact = call.remoteContact - val conferenceAddress = if (remoteContact != null) { - coreContext.core.interpretUrl( - remoteContact, - false - ) - } else { - null - } - val conferenceInfo = if (conferenceAddress != null) { - coreContext.core.findConferenceInformationFromUri( - conferenceAddress - ) - } else { - null - } - if (conferenceInfo == null) { - Log.i( - "[Notifications Manager] No conference info found for remote contact address $remoteContact" - ) - contact = coreContext.contactsManager.findContactByAddress(call.remoteAddress) - roundPicture = - ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri()) - displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress) - address = LinphoneUtils.getDisplayableAddress(call.remoteAddress) - info = context.getString(R.string.incoming_call_notification_title) - } else { - contact = null - displayName = conferenceInfo.subject ?: context.getString(R.string.conference) - address = LinphoneUtils.getDisplayableAddress(conferenceInfo.organizer) - roundPicture = coreContext.contactsManager.groupBitmap - info = context.getString(R.string.incoming_group_call_notification_title) - Log.i( - "[Notifications Manager] Displaying incoming group call notification with subject $displayName for remote contact address $remoteContact" - ) - } - - val notificationLayoutHeadsUp = RemoteViews( - context.packageName, - R.layout.call_incoming_notification_heads_up - ) - notificationLayoutHeadsUp.setTextViewText(R.id.caller, displayName) - notificationLayoutHeadsUp.setTextViewText(R.id.sip_uri, address) - notificationLayoutHeadsUp.setTextViewText(R.id.incoming_call_info, info) - - if (roundPicture != null) { - notificationLayoutHeadsUp.setImageViewBitmap(R.id.caller_picture, roundPicture) - } - - val builder = NotificationCompat.Builder( - context, - context.getString(R.string.notification_channel_incoming_call_id) - ) - .setStyle(NotificationCompat.DecoratedCustomViewStyle()) - .addPerson(notificationsManager.getPerson(contact, displayName, roundPicture)) - .setSmallIcon(R.drawable.topbar_call_notification) - .setContentTitle(displayName) - .setContentText(context.getString(R.string.incoming_call_notification_title)) - .setCategory(NotificationCompat.CATEGORY_CALL) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setWhen(System.currentTimeMillis()) - .setAutoCancel(false) - .setShowWhen(true) - .setOngoing(true) - .setColor(ContextCompat.getColor(context, R.color.primary_color)) - .setFullScreenIntent(pendingIntent, true) - .addAction(notificationsManager.getCallDeclineAction(notifiable)) - .addAction(notificationsManager.getCallAnswerAction(notifiable)) - .setCustomHeadsUpContentView(notificationLayoutHeadsUp) - - if (!corePreferences.preventInterfaceFromShowingUp) { - builder.setContentIntent(pendingIntent) - } - - return builder.build() - } - - fun createCallNotification( - context: Context, - call: Call, - notifiable: Notifiable, - pendingIntent: PendingIntent, - channel: String, - notificationsManager: NotificationsManager - ): Notification { - @StringRes val stringResourceId: Int - val iconResourceId: Int - val roundPicture: Bitmap? - val title: String - val person: Person - - val conferenceAddress = LinphoneUtils.getConferenceAddress(call) - val conferenceInfo = if (conferenceAddress != null) { - coreContext.core.findConferenceInformationFromUri( - conferenceAddress - ) - } else { - null - } - if (conferenceInfo != null) { - Log.i( - "[Notifications Manager] Displaying group call notification with subject ${conferenceInfo.subject}" - ) - } else { - Log.i( - "[Notifications Manager] No conference info found for remote contact address ${call.remoteAddress} (${call.remoteContact})" - ) - } - - if (conferenceInfo == null) { - val contact: Friend? = - coreContext.contactsManager.findContactByAddress(call.remoteAddress) - roundPicture = ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri()) - val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress) - title = contact?.name ?: displayName - person = notificationsManager.getPerson(contact, displayName, roundPicture) - } else { - title = conferenceInfo.subject ?: context.getString(R.string.conference) - roundPicture = coreContext.contactsManager.groupBitmap - person = Person.Builder() - .setName(title) - .setIcon(IconCompat.createWithBitmap(roundPicture)) - .build() - } - - when (call.state) { - Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote -> { - stringResourceId = R.string.call_notification_paused - iconResourceId = R.drawable.topbar_call_paused_notification - } - Call.State.OutgoingRinging, Call.State.OutgoingProgress, Call.State.OutgoingInit, Call.State.OutgoingEarlyMedia -> { - stringResourceId = R.string.call_notification_outgoing - iconResourceId = if (call.params.isVideoEnabled) { - R.drawable.topbar_videocall_notification - } else { - R.drawable.topbar_call_notification - } - } - else -> { - stringResourceId = R.string.call_notification_active - iconResourceId = if (call.currentParams.isVideoEnabled) { - R.drawable.topbar_videocall_notification - } else { - R.drawable.topbar_call_notification - } - } - } - - val builder = NotificationCompat.Builder( - context, - channel - ) - .setContentTitle(title) - .setContentText(context.getString(stringResourceId)) - .setSmallIcon(iconResourceId) - .setLargeIcon(roundPicture) - .addPerson(person) - .setAutoCancel(false) - .setCategory(NotificationCompat.CATEGORY_CALL) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setPriority(NotificationCompat.PRIORITY_LOW) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setOngoing(true) - .setColor(ContextCompat.getColor(context, R.color.notification_led_color)) - .addAction(notificationsManager.getCallDeclineAction(notifiable)) - - if (!corePreferences.preventInterfaceFromShowingUp) { - builder.setContentIntent(pendingIntent) - } - - return builder.build() - } - - @SuppressLint("MissingPermission") - fun eventVibration(vibrator: Vibrator) { - val effect = VibrationEffect.createWaveform( - longArrayOf(0L, 100L, 100L), - intArrayOf(0, VibrationEffect.DEFAULT_AMPLITUDE, 0), - -1 - ) - val audioAttrs = AudioAttributes.Builder() - .setUsage(AudioAttributes.USAGE_NOTIFICATION_EVENT) - .build() - vibrator.vibrate(effect, audioAttrs) - } - - fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int): Boolean { - Log.i( - "[Telecom Helper] Changing audio route [${routeToString(route)}] on connection [${connection.callId}] with state [${connection.stateAsString()}]" - ) - - val audioState = connection.callAudioState - if (audioState != null) { - Log.i("[Telecom Helper] Current audio route is ${routeToString(audioState.route)}") - if (audioState.route == route) { - Log.w("[Telecom Helper] Connection is already using this route") - return false - } - } else { - Log.w("[Telecom Helper] Failed to retrieve connection's call audio state!") - return false - } - - connection.setAudioRoute(route) - return true - } - - fun requestTelecomManagerPermission(activity: Activity, code: Int) { - activity.requestPermissions( - arrayOf( - Manifest.permission.READ_PHONE_STATE, - Manifest.permission.MANAGE_OWN_CALLS - ), - code - ) - } - - fun getImeFlagsForSecureChatRoom(): Int { - return EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING - } - - fun startForegroundService(context: Context, intent: Intent) { - context.startForegroundService(intent) - } - - fun hasTelecomManagerFeature(context: Context): Boolean { - return context.packageManager.hasSystemFeature( - PackageManager.FEATURE_CONNECTION_SERVICE - ) - } - - private fun routeToString(route: Int): String { - return when (route) { - CallAudioState.ROUTE_BLUETOOTH -> "BLUETOOTH" - CallAudioState.ROUTE_EARPIECE -> "EARPIECE" - CallAudioState.ROUTE_SPEAKER -> "SPEAKER" - CallAudioState.ROUTE_WIRED_HEADSET -> "WIRED_HEADSET" - CallAudioState.ROUTE_WIRED_OR_EARPIECE -> "WIRED_OR_EARPIECE" - else -> "Unknown: $route" - } - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api27Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api27Compatibility.kt deleted file mode 100644 index 55724fa6c..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api27Compatibility.kt +++ /dev/null @@ -1,42 +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 . - */ -package org.linphone.compatibility - -import android.annotation.TargetApi -import android.app.* -import android.content.Context - -@TargetApi(27) -class Api27Compatibility { - companion object { - fun setShowWhenLocked(activity: Activity, enable: Boolean) { - activity.setShowWhenLocked(enable) - } - - fun setTurnScreenOn(activity: Activity, enable: Boolean) { - activity.setTurnScreenOn(enable) - } - - fun requestDismissKeyguard(activity: Activity) { - val keyguardManager = activity.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager - keyguardManager.requestDismissKeyguard(activity, null) - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt deleted file mode 100644 index cccd52d54..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt +++ /dev/null @@ -1,32 +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 . - */ -package org.linphone.compatibility - -import android.annotation.TargetApi -import android.content.ClipboardManager - -@TargetApi(28) -class Api28Compatibility { - companion object { - fun clearClipboard(clipboard: ClipboardManager) { - clipboard.clearPrimaryClip() - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt deleted file mode 100644 index ee6ad686c..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api29Compatibility.kt +++ /dev/null @@ -1,268 +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 . - */ -package org.linphone.compatibility - -import android.Manifest -import android.annotation.TargetApi -import android.app.NotificationChannel -import android.app.NotificationManager -import android.content.ContentValues -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.ImageDecoder -import android.net.Uri -import android.os.Environment -import android.provider.MediaStore -import android.view.View -import android.view.contentcapture.ContentCaptureContext -import android.view.contentcapture.ContentCaptureSession -import androidx.core.app.NotificationManagerCompat -import org.linphone.R -import org.linphone.core.ChatRoom -import org.linphone.core.Content -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils -import org.linphone.utils.FileUtils -import org.linphone.utils.LinphoneUtils - -@TargetApi(29) -class Api29Compatibility { - companion object { - fun hasReadPhoneStatePermission(context: Context): Boolean { - val granted = Compatibility.hasPermission(context, Manifest.permission.READ_PHONE_STATE) - if (granted) { - Log.d("[Permission Helper] Permission READ_PHONE_STATE is granted") - } else { - Log.w("[Permission Helper] Permission READ_PHONE_STATE is denied") - } - return granted - } - - fun hasTelecomManagerPermission(context: Context): Boolean { - return Compatibility.hasPermission(context, Manifest.permission.READ_PHONE_STATE) && - Compatibility.hasPermission(context, Manifest.permission.MANAGE_OWN_CALLS) - } - - fun createMessageChannel( - context: Context, - notificationManager: NotificationManagerCompat - ) { - // Create messages notification channel - val id = context.getString(R.string.notification_channel_chat_id) - val name = context.getString(R.string.notification_channel_chat_name) - val description = context.getString(R.string.notification_channel_chat_name) - val channel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH) - channel.description = description - channel.lightColor = context.getColor(R.color.notification_led_color) - channel.enableLights(true) - channel.enableVibration(true) - channel.setShowBadge(true) - channel.setAllowBubbles(true) - notificationManager.createNotificationChannel(channel) - } - - fun extractLocusIdFromIntent(intent: Intent): String? { - return intent.getStringExtra(Intent.EXTRA_LOCUS_ID) - } - - fun setLocusIdInContentCaptureSession(root: View, chatRoom: ChatRoom) { - val session: ContentCaptureSession? = root.contentCaptureSession - if (session != null) { - val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress) - session.contentCaptureContext = ContentCaptureContext.forLocusId(id) - } - } - - fun canChatMessageChannelBubble(context: Context): Boolean { - val notificationManager: NotificationManager = - context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val bubblesAllowed = notificationManager.areBubblesAllowed() - Log.i( - "[Notifications Manager] Bubbles notifications are ${if (bubblesAllowed) "allowed" else "forbidden"}" - ) - return bubblesAllowed - } - - fun getBitmapFromUri(context: Context, uri: Uri): Bitmap { - return ImageDecoder.decodeBitmap( - ImageDecoder.createSource(context.contentResolver, uri) - ) - } - - suspend fun addImageToMediaStore(context: Context, content: Content): Boolean { - return addContentToMediaStore(context, content, isImage = true) - } - - suspend fun addVideoToMediaStore(context: Context, content: Content): Boolean { - return addContentToMediaStore(context, content, isVideo = true) - } - - suspend fun addAudioToMediaStore(context: Context, content: Content): Boolean { - return addContentToMediaStore(context, content, isAudio = true) - } - - private suspend fun addContentToMediaStore( - context: Context, - content: Content, - isImage: Boolean = false, - isVideo: Boolean = false, - isAudio: Boolean = false - ): Boolean { - val isContentEncrypted = content.isFileEncrypted - val filePath = if (content.isFileEncrypted) { - val plainFilePath = content.exportPlainFile().orEmpty() - Log.i( - "[Media Store] [VFS] Content is encrypted, plain file path is: $plainFilePath" - ) - plainFilePath - } else { - content.filePath - } - - if (filePath.isNullOrEmpty()) { - Log.e("[Media Store] Content doesn't have a file path!") - return false - } - - val appName = AppUtils.getString(R.string.app_name) - val directory = when { - isImage -> Environment.DIRECTORY_PICTURES - isVideo -> Environment.DIRECTORY_MOVIES - isAudio -> Environment.DIRECTORY_MUSIC - else -> Environment.DIRECTORY_DOWNLOADS - } - val relativePath = "$directory/$appName" - val fileName = content.name - val mime = "${content.type}/${content.subtype}" - val type = when { - isImage -> "image" - isVideo -> "video" - isAudio -> "audio" - else -> "unexpected" - } - Log.i( - "[Media Store] Adding $type [$filePath] to Media Store with name [$fileName] and MIME [$mime], asking to be stored in: $relativePath" - ) - - val mediaStoreFilePath = when { - isImage -> { - val values = ContentValues().apply { - put(MediaStore.Images.Media.DISPLAY_NAME, fileName) - put(MediaStore.Images.Media.MIME_TYPE, mime) - put(MediaStore.Images.Media.RELATIVE_PATH, relativePath) - put(MediaStore.Images.Media.IS_PENDING, 1) - } - val collection = MediaStore.Images.Media.getContentUri( - MediaStore.VOLUME_EXTERNAL_PRIMARY - ) - addContentValuesToCollection( - context, - filePath, - collection, - values, - MediaStore.Images.Media.IS_PENDING - ) - } - isVideo -> { - val values = ContentValues().apply { - put(MediaStore.Video.Media.TITLE, fileName) - put(MediaStore.Video.Media.DISPLAY_NAME, fileName) - put(MediaStore.Video.Media.MIME_TYPE, mime) - put(MediaStore.Video.Media.RELATIVE_PATH, relativePath) - put(MediaStore.Video.Media.IS_PENDING, 1) - } - val collection = MediaStore.Video.Media.getContentUri( - MediaStore.VOLUME_EXTERNAL_PRIMARY - ) - addContentValuesToCollection( - context, - filePath, - collection, - values, - MediaStore.Video.Media.IS_PENDING - ) - } - isAudio -> { - val values = ContentValues().apply { - put(MediaStore.Audio.Media.TITLE, fileName) - put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) - put(MediaStore.Audio.Media.MIME_TYPE, mime) - put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath) - put(MediaStore.Audio.Media.IS_PENDING, 1) - } - val collection = MediaStore.Audio.Media.getContentUri( - MediaStore.VOLUME_EXTERNAL_PRIMARY - ) - addContentValuesToCollection( - context, - filePath, - collection, - values, - MediaStore.Audio.Media.IS_PENDING - ) - } - else -> "" - } - - if (isContentEncrypted) { - Log.w("[Media Store] Content was encrypted, delete plain version: $filePath") - FileUtils.deleteFile(filePath) - } - - if (mediaStoreFilePath.isNotEmpty()) { - Log.i("[Media Store] Exported file path is: $mediaStoreFilePath") - content.userData = mediaStoreFilePath - return true - } - - return false - } - - private suspend fun addContentValuesToCollection( - context: Context, - filePath: String, - collection: Uri, - values: ContentValues, - pendingKey: String - ): String { - try { - val fileUri = context.contentResolver.insert(collection, values) - if (fileUri == null) { - Log.e("[Media Store] Failed to get a URI to where store the file, aborting") - return "" - } - - context.contentResolver.openOutputStream(fileUri).use { out -> - if (FileUtils.copyFileTo(filePath, out)) { - values.clear() - values.put(pendingKey, 0) - context.contentResolver.update(fileUri, values, null, null) - - return fileUri.toString() - } - } - } catch (e: Exception) { - Log.e("[Media Store] Exception: $e") - } - return "" - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt deleted file mode 100644 index 7d41cb093..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api30Compatibility.kt +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright (c) 2010-2020 Belledonne Communications SARL. - * - * This file is part of linphone-android - * (see https://www.linphone.org). - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.linphone.compatibility - -import android.Manifest -import android.annotation.TargetApi -import android.app.Activity -import android.content.Context -import android.content.pm.ShortcutManager -import android.view.Window -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat -import androidx.fragment.app.Fragment -import org.linphone.core.ChatRoom -import org.linphone.core.tools.Log -import org.linphone.utils.LinphoneUtils - -@TargetApi(30) -class Api30Compatibility { - companion object { - fun hasReadPhoneNumbersPermission(context: Context): Boolean { - val granted = Compatibility.hasPermission( - context, - Manifest.permission.READ_PHONE_NUMBERS - ) - if (granted) { - Log.d("[Permission Helper] Permission READ_PHONE_NUMBERS is granted") - } else { - Log.w("[Permission Helper] Permission READ_PHONE_NUMBERS is denied") - } - return granted - } - - fun requestReadPhoneNumbersPermission(fragment: Fragment, code: Int) { - fragment.requestPermissions(arrayOf(Manifest.permission.READ_PHONE_NUMBERS), code) - } - - fun requestTelecomManagerPermission(activity: Activity, code: Int) { - activity.requestPermissions( - arrayOf( - Manifest.permission.READ_PHONE_NUMBERS, - Manifest.permission.READ_PHONE_STATE, - Manifest.permission.MANAGE_OWN_CALLS - ), - code - ) - } - - fun hasTelecomManagerPermission(context: Context): Boolean { - return Compatibility.hasPermission(context, Manifest.permission.READ_PHONE_NUMBERS) && - Compatibility.hasPermission(context, Manifest.permission.READ_PHONE_STATE) && - Compatibility.hasPermission(context, Manifest.permission.MANAGE_OWN_CALLS) - } - - fun removeChatRoomShortcut(context: Context, chatRoom: ChatRoom) { - val shortcutManager = context.getSystemService(ShortcutManager::class.java) - val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress) - val shortcutsToRemoveList = arrayListOf(id) - shortcutManager.removeLongLivedShortcuts(shortcutsToRemoveList) - } - - fun hideAndroidSystemUI(hide: Boolean, window: Window) { - val windowInsetsCompat = WindowInsetsControllerCompat(window, window.decorView) - if (hide) { - WindowCompat.setDecorFitsSystemWindows(window, false) - windowInsetsCompat.let { - it.hide(WindowInsetsCompat.Type.systemBars()) - it.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } - } else { - windowInsetsCompat.show(WindowInsetsCompat.Type.systemBars()) - WindowCompat.setDecorFitsSystemWindows(window, true) - } - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt deleted file mode 100644 index e91b57f4c..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt +++ /dev/null @@ -1,305 +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 . - */ -package org.linphone.compatibility - -import android.annotation.TargetApi -import android.app.* -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import androidx.core.content.ContextCompat -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.contact.getThumbnailUri -import org.linphone.core.Call -import org.linphone.core.tools.Log -import org.linphone.notifications.Notifiable -import org.linphone.notifications.NotificationsManager -import org.linphone.utils.ImageUtils -import org.linphone.utils.LinphoneUtils - -@TargetApi(31) -class Api31Compatibility { - companion object { - fun getUpdateCurrentPendingIntentFlag(): Int { - return PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - } - - fun createIncomingCallNotification( - context: Context, - call: Call, - notifiable: Notifiable, - pendingIntent: PendingIntent, - notificationsManager: NotificationsManager - ): Notification { - val remoteContact = call.remoteContact - val conferenceAddress = if (remoteContact != null) { - coreContext.core.interpretUrl( - remoteContact, - false - ) - } else { - null - } - val conferenceInfo = if (conferenceAddress != null) { - coreContext.core.findConferenceInformationFromUri( - conferenceAddress - ) - } else { - null - } - if (conferenceInfo != null) { - Log.i( - "[Notifications Manager] Displaying incoming group call notification with subject ${conferenceInfo.subject} and remote contact address $remoteContact" - ) - } else { - Log.i( - "[Notifications Manager] No conference info found for remote contact address $remoteContact" - ) - } - - val caller = if (conferenceInfo == null) { - val contact = - coreContext.contactsManager.findContactByAddress(call.remoteAddress) - val roundPicture = - ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri()) - val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress) - - val person = notificationsManager.getPerson(contact, displayName, roundPicture) - Person.Builder() - .setName(person.name) - .setIcon(person.icon?.toIcon(context)) - .setUri(person.uri) - .setKey(person.key) - .setImportant(person.isImportant) - .build() - } else { - Person.Builder() - .setName( - if (conferenceInfo.subject.isNullOrEmpty()) { - context.getString( - R.string.conference_incoming_title - ) - } else { - conferenceInfo.subject - } - ) - .setIcon(coreContext.contactsManager.groupAvatar.toIcon(context)) - .setImportant(false) - .build() - } - - val declineIntent = notificationsManager.getCallDeclinePendingIntent(notifiable) - val answerIntent = notificationsManager.getCallAnswerPendingIntent(notifiable) - - val isVideoEnabledInRemoteParams = call.remoteParams?.isVideoEnabled ?: false - val isVideoAutomaticallyAccepted = call.core.videoActivationPolicy.automaticallyAccept - val isVideo = isVideoEnabledInRemoteParams && isVideoAutomaticallyAccepted - - val builder = Notification.Builder( - context, - context.getString(R.string.notification_channel_incoming_call_id) - ).apply { - try { - style = Notification.CallStyle.forIncomingCall( - caller, - declineIntent, - answerIntent - ).setIsVideo(isVideo) - } catch (iae: IllegalArgumentException) { - Log.e( - "[Api31 Compatibility] Can't use notification call style: $iae, using API 26 notification instead" - ) - return Api26Compatibility.createIncomingCallNotification( - context, - call, - notifiable, - pendingIntent, - notificationsManager - ) - } - setSmallIcon(R.drawable.topbar_call_notification) - setCategory(Notification.CATEGORY_CALL) - setVisibility(Notification.VISIBILITY_PUBLIC) - setWhen(System.currentTimeMillis()) - setAutoCancel(false) - setShowWhen(true) - setOngoing(true) - setColor(ContextCompat.getColor(context, R.color.primary_color)) - setFullScreenIntent(pendingIntent, true) - } - - if (!corePreferences.preventInterfaceFromShowingUp) { - builder.setContentIntent(pendingIntent) - } - - return builder.build() - } - - fun createCallNotification( - context: Context, - call: Call, - notifiable: Notifiable, - pendingIntent: PendingIntent, - channel: String, - notificationsManager: NotificationsManager - ): Notification { - val conferenceAddress = LinphoneUtils.getConferenceAddress(call) - val conferenceInfo = if (conferenceAddress != null) { - coreContext.core.findConferenceInformationFromUri( - conferenceAddress - ) - } else { - null - } - if (conferenceInfo != null) { - Log.i( - "[Notifications Manager] Displaying group call notification with subject ${conferenceInfo.subject}" - ) - } else { - Log.i( - "[Notifications Manager] No conference info found for remote contact address ${call.remoteAddress} (${call.remoteContact})" - ) - } - - val caller = if (conferenceInfo == null) { - val contact = - coreContext.contactsManager.findContactByAddress(call.remoteAddress) - val roundPicture = - ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri()) - val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress) - - val person = notificationsManager.getPerson(contact, displayName, roundPicture) - Person.Builder() - .setName(person.name) - .setIcon(person.icon?.toIcon(context)) - .setUri(person.uri) - .setKey(person.key) - .setImportant(person.isImportant) - .build() - } else { - Person.Builder() - .setName(conferenceInfo.subject) - .setIcon(coreContext.contactsManager.groupAvatar.toIcon(context)) - .setImportant(false) - .build() - } - val isVideo = call.currentParams.isVideoEnabled - val iconResourceId: Int = when (call.state) { - Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote -> { - R.drawable.topbar_call_paused_notification - } - else -> { - if (isVideo) { - R.drawable.topbar_videocall_notification - } else { - R.drawable.topbar_call_notification - } - } - } - val declineIntent = notificationsManager.getCallDeclinePendingIntent(notifiable) - - val builder = Notification.Builder( - context, - channel - ).apply { - try { - style = Notification.CallStyle.forOngoingCall(caller, declineIntent) - .setIsVideo(isVideo) - } catch (iae: IllegalArgumentException) { - Log.e( - "[Api31 Compatibility] Can't use notification call style: $iae, using API 26 notification instead" - ) - return Api26Compatibility.createCallNotification( - context, - call, - notifiable, - pendingIntent, - channel, - notificationsManager - ) - } - setSmallIcon(iconResourceId) - setAutoCancel(false) - setCategory(Notification.CATEGORY_CALL) - setVisibility(Notification.VISIBILITY_PUBLIC) - setWhen(System.currentTimeMillis()) - setShowWhen(true) - setOngoing(true) - setColor(ContextCompat.getColor(context, R.color.notification_led_color)) - // This is required for CallStyle notification - setFullScreenIntent(pendingIntent, true) - } - - if (!corePreferences.preventInterfaceFromShowingUp) { - builder.setContentIntent(pendingIntent) - } - - return builder.build() - } - - fun startForegroundService(context: Context, intent: Intent) { - try { - context.startForegroundService(intent) - } catch (fssnae: ForegroundServiceStartNotAllowedException) { - Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae") - } catch (se: SecurityException) { - Log.e("[Api31 Compatibility] Can't start service as foreground! $se") - } catch (e: Exception) { - Log.e("[Api31 Compatibility] Can't start service as foreground! $e") - } - } - - fun startForegroundService(service: Service, notifId: Int, notif: Notification?) { - try { - service.startForeground(notifId, notif) - } catch (fssnae: ForegroundServiceStartNotAllowedException) { - Log.e("[Api31 Compatibility] Can't start service as foreground! $fssnae") - } catch (se: SecurityException) { - Log.e("[Api31 Compatibility] Can't start service as foreground! $se") - } catch (e: Exception) { - Log.e("[Api31 Compatibility] Can't start service as foreground! $e") - } - } - - fun enableAutoEnterPiP(activity: Activity, enable: Boolean, conference: Boolean) { - val supportsPip = activity.packageManager - .hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) - Log.i("[Call] Is PiP supported: $supportsPip") - if (supportsPip) { - // Force portrait layout if in conference, otherwise for landscape - // Our layouts behave better in these orientation - val params = PictureInPictureParams.Builder() - .setAutoEnterEnabled(enable) - .setAspectRatio(Compatibility.getPipRatio(activity, conference, !conference)) - .build() - try { - activity.setPictureInPictureParams(params) - Log.i( - "[Call] PiP auto enter enabled params set to $enable with ${if (conference) "portrait" else "landscape"} aspect ratio" - ) - } catch (e: Exception) { - Log.e("[Call] Can't build PiP params: $e") - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt deleted file mode 100644 index 2a524fa34..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api33Compatibility.kt +++ /dev/null @@ -1,66 +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 . - */ -package org.linphone.compatibility - -import android.Manifest -import android.annotation.TargetApi -import android.content.Context -import android.content.pm.PackageManager -import androidx.fragment.app.Fragment - -@TargetApi(33) -class Api33Compatibility { - companion object { - fun requestPostNotificationsPermission(fragment: Fragment, code: Int) { - fragment.requestPermissions( - arrayOf( - Manifest.permission.POST_NOTIFICATIONS - ), - code - ) - } - - fun hasPostNotificationsPermission(context: Context): Boolean { - return Compatibility.hasPermission(context, Manifest.permission.POST_NOTIFICATIONS) - } - - fun requestReadMediaAndCameraPermissions(fragment: Fragment, code: Int) { - fragment.requestPermissions( - arrayOf( - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_VIDEO, - Manifest.permission.READ_MEDIA_AUDIO, - Manifest.permission.CAMERA - ), - code - ) - } - - fun hasReadExternalStoragePermission(context: Context): Boolean { - return Compatibility.hasPermission(context, Manifest.permission.READ_MEDIA_IMAGES) || - Compatibility.hasPermission(context, Manifest.permission.READ_MEDIA_VIDEO) || - Compatibility.hasPermission(context, Manifest.permission.READ_MEDIA_AUDIO) - } - - fun hasTelecomManagerFeature(context: Context): Boolean { - return context.packageManager.hasSystemFeature(PackageManager.FEATURE_TELECOM) - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt deleted file mode 100644 index 79c164dea..000000000 --- a/app/src/main/java/org/linphone/compatibility/Api34Compatibility.kt +++ /dev/null @@ -1,146 +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 . - */ -package org.linphone.compatibility - -import android.annotation.TargetApi -import android.app.ForegroundServiceStartNotAllowedException -import android.app.Notification -import android.app.NotificationManager -import android.app.Service -import android.content.Context -import android.content.Intent -import android.content.pm.ServiceInfo -import android.net.Uri -import android.provider.Settings -import androidx.core.content.ContextCompat -import org.linphone.core.tools.Log -import org.linphone.utils.PermissionHelper - -@TargetApi(34) -class Api34Compatibility { - companion object { - fun hasFullScreenIntentPermission(context: Context): Boolean { - val notificationManager = context.getSystemService(NotificationManager::class.java) as NotificationManager - // See https://developer.android.com/reference/android/app/NotificationManager#canUseFullScreenIntent%28%29 - return notificationManager.canUseFullScreenIntent() - } - - fun requestFullScreenIntentPermission(context: Context) { - val intent = Intent() - // See https://developer.android.com/reference/android/provider/Settings#ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT - intent.action = Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT - intent.data = Uri.parse("package:${context.packageName}") - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) - ContextCompat.startActivity(context, intent, null) - } - - fun startCallForegroundService( - service: Service, - notifId: Int, - notif: Notification, - isCallActive: Boolean - ) { - val mask = if (isCallActive) { - Log.i( - "[Api34 Compatibility] Trying to start service as foreground using at least FOREGROUND_SERVICE_TYPE_PHONE_CALL" - ) - var computeMask = ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL - if (PermissionHelper.get().hasCameraPermission()) { - Log.i( - "[Api34 Compatibility] CAMERA permission has been granted, adding FOREGROUND_SERVICE_TYPE_CAMERA" - ) - computeMask = computeMask or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA - } - if (PermissionHelper.get().hasRecordAudioPermission()) { - Log.i( - "[Api34 Compatibility] RECORD_AUDIO permission has been granted, adding FOREGROUND_SERVICE_TYPE_MICROPHONE" - ) - computeMask = computeMask or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE - } - computeMask - } else { - Log.i( - "[Api34 Compatibility] Trying to start service as foreground using only FOREGROUND_SERVICE_TYPE_PHONE_CALL because call isn't active yet" - ) - ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL - } - - try { - service.startForeground( - notifId, - notif, - mask - ) - } catch (fssnae: ForegroundServiceStartNotAllowedException) { - Log.e("[Api34 Compatibility] Can't start service as foreground! $fssnae") - } catch (se: SecurityException) { - Log.e("[Api34 Compatibility] Can't start service as foreground! $se") - } catch (e: Exception) { - Log.e("[Api34 Compatibility] Can't start service as foreground! $e") - } - } - - fun startDataSyncForegroundService( - service: Service, - notifId: Int, - notif: Notification, - isCallActive: Boolean - ) { - val mask = if (isCallActive) { - Log.i( - "[Api34 Compatibility] Trying to start service as foreground using at least FOREGROUND_SERVICE_TYPE_PHONE_CALL or FOREGROUND_SERVICE_TYPE_DATA_SYNC" - ) - var computeMask = ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL or ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC - if (PermissionHelper.get().hasCameraPermission()) { - Log.i( - "[Api34 Compatibility] CAMERA permission has been granted, adding FOREGROUND_SERVICE_TYPE_CAMERA" - ) - computeMask = computeMask or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA - } - if (PermissionHelper.get().hasRecordAudioPermission()) { - Log.i( - "[Api34 Compatibility] RECORD_AUDIO permission has been granted, adding FOREGROUND_SERVICE_TYPE_MICROPHONE" - ) - computeMask = computeMask or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE - } - computeMask - } else { - Log.i( - "[Api34 Compatibility] Trying to start service as foreground using only FOREGROUND_SERVICE_TYPE_DATA_SYNC because no call at the time" - ) - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC - } - - try { - service.startForeground( - notifId, - notif, - mask - ) - } catch (fssnae: ForegroundServiceStartNotAllowedException) { - Log.e("[Api34 Compatibility] Can't start service as foreground! $fssnae") - } catch (se: SecurityException) { - Log.e("[Api34 Compatibility] Can't start service as foreground! $se") - } catch (e: Exception) { - Log.e("[Api34 Compatibility] Can't start service as foreground! $e") - } - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt deleted file mode 100644 index c0557bbcd..000000000 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ /dev/null @@ -1,503 +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 . - */ -package org.linphone.compatibility - -import android.app.Activity -import android.app.Notification -import android.app.PendingIntent -import android.app.Service -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri -import android.os.Build -import android.os.Vibrator -import android.telephony.TelephonyManager -import android.util.DisplayMetrics -import android.util.Rational -import android.view.View -import android.view.Window -import android.view.WindowManager -import androidx.core.app.NotificationManagerCompat -import androidx.fragment.app.Fragment -import java.util.* -import org.linphone.core.Call -import org.linphone.core.ChatRoom -import org.linphone.core.Content -import org.linphone.mediastream.Version -import org.linphone.notifications.Notifiable -import org.linphone.notifications.NotificationsManager -import org.linphone.telecom.NativeCallWrapper - -@Suppress("DEPRECATION") -class Compatibility { - companion object { - fun hasPermission(context: Context, permission: String): Boolean { - return Api23Compatibility.hasPermission(context, permission) - } - - // See https://developer.android.com/about/versions/11/privacy/permissions#phone-numbers - fun hasReadPhoneStateOrNumbersPermission(context: Context): Boolean { - return if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) { - Api30Compatibility.hasReadPhoneNumbersPermission(context) - } else { - Api29Compatibility.hasReadPhoneStatePermission(context) - } - } - - // See https://developer.android.com/about/versions/11/privacy/permissions#phone-numbers - fun requestReadPhoneStateOrNumbersPermission(fragment: Fragment, code: Int) { - if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) { - Api30Compatibility.requestReadPhoneNumbersPermission(fragment, code) - } else { - Api23Compatibility.requestReadPhoneStatePermission(fragment, code) - } - } - - // See https://developer.android.com/about/versions/11/privacy/permissions#phone-numbers - fun hasTelecomManagerPermissions(context: Context): Boolean { - return if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) { - Api30Compatibility.hasTelecomManagerPermission(context) - } else if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - Api29Compatibility.hasTelecomManagerPermission(context) - } else { - false - } - } - - fun requestTelecomManagerPermissions(activity: Activity, code: Int) { - if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) { - Api30Compatibility.requestTelecomManagerPermission(activity, code) - } else { - Api26Compatibility.requestTelecomManagerPermission(activity, code) - } - } - - fun requestPostNotificationsPermission(fragment: Fragment, code: Int) { - if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) { - Api33Compatibility.requestPostNotificationsPermission(fragment, code) - } - } - - fun hasPostNotificationsPermission(context: Context): Boolean { - return if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) { - Api33Compatibility.hasPostNotificationsPermission(context) - } else { - true - } - } - - fun requestReadExternalStorageAndCameraPermissions(fragment: Fragment, code: Int) { - if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) { - Api33Compatibility.requestReadMediaAndCameraPermissions(fragment, code) - } else { - Api23Compatibility.requestReadExternalStorageAndCameraPermissions(fragment, code) - } - } - - fun hasReadExternalStoragePermission(context: Context): Boolean { - return if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) { - Api33Compatibility.hasReadExternalStoragePermission(context) - } else { - Api23Compatibility.hasReadExternalStoragePermission(context) - } - } - - fun getDeviceName(context: Context): String { - return when (Version.sdkAboveOrEqual(Version.API25_NOUGAT_71)) { - true -> Api25Compatibility.getDeviceName(context) - else -> Api23Compatibility.getDeviceName(context) - } - } - - fun createPhoneListener(telephonyManager: TelephonyManager): PhoneStateInterface { - return if (Version.sdkStrictlyBelow(Version.API31_ANDROID_12)) { - PhoneStateListener(telephonyManager) - } else { - TelephonyListener(telephonyManager) - } - } - - /* UI */ - - fun setShowWhenLocked(activity: Activity, enable: Boolean) { - if (Version.sdkStrictlyBelow(Version.API27_OREO_81)) { - Api23Compatibility.setShowWhenLocked(activity, enable) - } else { - Api27Compatibility.setShowWhenLocked(activity, enable) - } - } - - fun setTurnScreenOn(activity: Activity, enable: Boolean) { - if (Version.sdkStrictlyBelow(Version.API27_OREO_81)) { - Api23Compatibility.setTurnScreenOn(activity, enable) - } else { - Api27Compatibility.setTurnScreenOn(activity, enable) - } - } - - fun requestDismissKeyguard(activity: Activity) { - if (Version.sdkStrictlyBelow(Version.API27_OREO_81)) { - Api23Compatibility.requestDismissKeyguard(activity) - } else { - Api27Compatibility.requestDismissKeyguard(activity) - } - } - - fun getBitmapFromUri(context: Context, uri: Uri): Bitmap { - return if (Version.sdkStrictlyBelow(Version.API29_ANDROID_10)) { - Api23Compatibility.getBitmapFromUri(context, uri) - } else { - Api29Compatibility.getBitmapFromUri(context, uri) - } - } - - /* Notifications */ - - fun createNotificationChannels( - context: Context, - notificationManager: NotificationManagerCompat - ) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - Api26Compatibility.createServiceChannel(context, notificationManager) - Api26Compatibility.createMissedCallChannel(context, notificationManager) - Api26Compatibility.createIncomingCallChannel(context, notificationManager) - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - Api29Compatibility.createMessageChannel(context, notificationManager) - } else { - Api26Compatibility.createMessageChannel(context, notificationManager) - } - } - } - - fun getChannelImportance( - notificationManager: NotificationManagerCompat, - channelId: String - ): Int { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return Api26Compatibility.getChannelImportance(notificationManager, channelId) - } - return NotificationManagerCompat.IMPORTANCE_DEFAULT - } - - fun getOverlayType(): Int { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return Api26Compatibility.getOverlayType() - } - return WindowManager.LayoutParams.TYPE_PHONE - } - - fun createIncomingCallNotification( - context: Context, - call: Call, - notifiable: Notifiable, - pendingIntent: PendingIntent, - notificationsManager: NotificationsManager - ): Notification { - val manufacturer = Build.MANUFACTURER.lowercase(Locale.getDefault()) - // Samsung One UI 4.0 (API 31) doesn't (currently) display CallStyle notifications well - // Tested on Samsung S10 and Z Fold 2 - if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12) && manufacturer != "samsung") { - return Api31Compatibility.createIncomingCallNotification( - context, - call, - notifiable, - pendingIntent, - notificationsManager - ) - } else if (manufacturer == "xiaomi") { // Xiaomi devices don't handle CustomHeadsUpContentView correctly - return XiaomiCompatibility.createIncomingCallNotification( - context, - call, - notifiable, - pendingIntent, - notificationsManager - ) - } - return Api26Compatibility.createIncomingCallNotification( - context, - call, - notifiable, - pendingIntent, - notificationsManager - ) - } - - fun createCallNotification( - context: Context, - call: Call, - notifiable: Notifiable, - pendingIntent: PendingIntent, - channel: String, - notificationsManager: NotificationsManager - ): Notification { - val manufacturer = Build.MANUFACTURER.lowercase(Locale.getDefault()) - // Samsung One UI 4.0 (API 31) doesn't (currently) display CallStyle notifications well - // Tested on Samsung S10 and Z Fold 2 - if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12) && manufacturer != "samsung") { - return Api31Compatibility.createCallNotification( - context, - call, - notifiable, - pendingIntent, - channel, - notificationsManager - ) - } - return Api26Compatibility.createCallNotification( - context, - call, - notifiable, - pendingIntent, - channel, - notificationsManager - ) - } - - fun startForegroundService(context: Context, intent: Intent) { - if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) { - Api31Compatibility.startForegroundService(context, intent) - } else if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - Api26Compatibility.startForegroundService(context, intent) - } else { - Api23Compatibility.startForegroundService(context, intent) - } - } - - private fun startForegroundService(service: Service, notifId: Int, notif: Notification?) { - if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) { - Api31Compatibility.startForegroundService(service, notifId, notif) - } else { - Api23Compatibility.startForegroundService(service, notifId, notif) - } - } - - fun startCallForegroundService( - service: Service, - notifId: Int, - notif: Notification, - isCallActive: Boolean - ) { - if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { - Api34Compatibility.startCallForegroundService(service, notifId, notif, isCallActive) - } else { - startForegroundService(service, notifId, notif) - } - } - - fun startDataSyncForegroundService( - service: Service, - notifId: Int, - notif: Notification, - isCallActive: Boolean - ) { - if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { - Api34Compatibility.startDataSyncForegroundService( - service, - notifId, - notif, - isCallActive - ) - } else { - startForegroundService(service, notifId, notif) - } - } - - /* Call */ - - fun canDrawOverlay(context: Context): Boolean { - return Api23Compatibility.canDrawOverlay(context) - } - - fun isInPictureInPictureMode(activity: Activity): Boolean { - if (Version.sdkAboveOrEqual(Version.API25_NOUGAT_71)) { - return Api25Compatibility.isInPictureInPictureMode(activity) - } - return false - } - - fun enterPipMode(activity: Activity, conference: Boolean) { - if (Version.sdkStrictlyBelow(Version.API31_ANDROID_12) && Version.sdkAboveOrEqual( - Version.API26_O_80 - ) - ) { - Api26Compatibility.enterPipMode(activity, conference) - } - } - - fun enableAutoEnterPiP(activity: Activity, enable: Boolean, conference: Boolean) { - if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) { - Api31Compatibility.enableAutoEnterPiP(activity, enable, conference) - } - } - - fun getPipRatio( - activity: Activity, - forcePortrait: Boolean = false, - forceLandscape: Boolean = false - ): Rational { - val displayMetrics = DisplayMetrics() - activity.windowManager.defaultDisplay.getMetrics(displayMetrics) - var height = displayMetrics.heightPixels - var width = displayMetrics.widthPixels - - val aspectRatio = width / height - if (aspectRatio < 1 / 2.39) { - height = 2.39.toInt() - width = 1 - } else if (aspectRatio > 2.39) { - width = 2.39.toInt() - height = 1 - } - - val ratio = if (width > height) { - if (forcePortrait) { - Rational(height, width) - } else { - Rational(width, height) - } - } else { - if (forceLandscape) { - Rational(height, width) - } else { - Rational(width, height) - } - } - return ratio - } - - fun eventVibration(vibrator: Vibrator) { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - Api26Compatibility.eventVibration(vibrator) - } else { - Api23Compatibility.eventVibration(vibrator) - } - } - - fun changeAudioRouteForTelecomManager(connection: NativeCallWrapper, route: Int): Boolean { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return Api26Compatibility.changeAudioRouteForTelecomManager(connection, route) - } - return false - } - - fun hideAndroidSystemUI(hide: Boolean, window: Window) { - if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) { - Api30Compatibility.hideAndroidSystemUI(hide, window) - } else { - Api23Compatibility.hideAndroidSystemUI(hide, window) - } - } - - /* Chat */ - - fun removeChatRoomShortcut(context: Context, chatRoom: ChatRoom) { - if (Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) { - Api30Compatibility.removeChatRoomShortcut(context, chatRoom) - } - } - - fun extractLocusIdFromIntent(intent: Intent): String? { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return Api29Compatibility.extractLocusIdFromIntent(intent) - } - return null - } - - fun setLocusIdInContentCaptureSession(root: View, chatRoom: ChatRoom) { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return Api29Compatibility.setLocusIdInContentCaptureSession(root, chatRoom) - } - } - - fun canChatMessageChannelBubble(context: Context): Boolean { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return Api29Compatibility.canChatMessageChannelBubble(context) - } - return false - } - - suspend fun addImageToMediaStore(context: Context, content: Content): Boolean { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return Api29Compatibility.addImageToMediaStore(context, content) - } - return Api23Compatibility.addImageToMediaStore(context, content) - } - - suspend fun addVideoToMediaStore(context: Context, content: Content): Boolean { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return Api29Compatibility.addVideoToMediaStore(context, content) - } - return Api23Compatibility.addVideoToMediaStore(context, content) - } - - suspend fun addAudioToMediaStore(context: Context, content: Content): Boolean { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) { - return Api29Compatibility.addAudioToMediaStore(context, content) - } - return Api23Compatibility.addAudioToMediaStore(context, content) - } - - fun getUpdateCurrentPendingIntentFlag(): Int { - if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) { - return Api31Compatibility.getUpdateCurrentPendingIntentFlag() - } - return Api23Compatibility.getUpdateCurrentPendingIntentFlag() - } - - fun getImeFlagsForSecureChatRoom(): Int { - if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return Api26Compatibility.getImeFlagsForSecureChatRoom() - } - return Api23Compatibility.getImeFlagsForSecureChatRoom() - } - - fun hasTelecomManagerFeature(context: Context): Boolean { - if (Version.sdkAboveOrEqual(Version.API33_ANDROID_13_TIRAMISU)) { - return Api33Compatibility.hasTelecomManagerFeature(context) - } else if (Version.sdkAboveOrEqual(Version.API26_O_80)) { - return Api26Compatibility.hasTelecomManagerFeature(context) - } - return false - } - - fun clearClipboard(clipboard: ClipboardManager) { - if (Version.sdkAboveOrEqual(Version.API28_PIE_90)) { - Api28Compatibility.clearClipboard(clipboard) - } - } - - fun hasFullScreenIntentPermission(context: Context): Boolean { - if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { - return Api34Compatibility.hasFullScreenIntentPermission(context) - } - return true - } - - fun requestFullScreenIntentPermission(context: Context): Boolean { - if (Version.sdkAboveOrEqual(Version.API34_ANDROID_14_UPSIDE_DOWN_CAKE)) { - Api34Compatibility.requestFullScreenIntentPermission(context) - return true - } - return false - } - } -} diff --git a/app/src/main/java/org/linphone/compatibility/PhoneStateInterface.kt b/app/src/main/java/org/linphone/compatibility/PhoneStateInterface.kt deleted file mode 100644 index 4d7723081..000000000 --- a/app/src/main/java/org/linphone/compatibility/PhoneStateInterface.kt +++ /dev/null @@ -1,26 +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 . - */ -package org.linphone.compatibility - -interface PhoneStateInterface { - fun destroy() - - fun isInCall(): Boolean -} diff --git a/app/src/main/java/org/linphone/compatibility/PhoneStateListener.kt b/app/src/main/java/org/linphone/compatibility/PhoneStateListener.kt deleted file mode 100644 index d5ca88920..000000000 --- a/app/src/main/java/org/linphone/compatibility/PhoneStateListener.kt +++ /dev/null @@ -1,65 +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 . - */ -package org.linphone.compatibility - -import android.telephony.PhoneStateListener -import android.telephony.TelephonyManager -import org.linphone.core.tools.Log - -class PhoneStateListener(private val telephonyManager: TelephonyManager) : PhoneStateInterface { - private var gsmCallActive = false - private val phoneStateListener = object : PhoneStateListener() { - @Deprecated("Deprecated in Java") - override fun onCallStateChanged(state: Int, phoneNumber: String?) { - gsmCallActive = when (state) { - TelephonyManager.CALL_STATE_OFFHOOK -> { - Log.i("[Context] Phone state is off hook") - true - } - TelephonyManager.CALL_STATE_RINGING -> { - Log.i("[Context] Phone state is ringing") - true - } - TelephonyManager.CALL_STATE_IDLE -> { - Log.i("[Context] Phone state is idle") - false - } - else -> { - Log.w("[Context] Phone state is unexpected: $state") - false - } - } - } - } - - init { - Log.i("[Phone State Listener] Registering phone state listener") - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE) - } - - override fun destroy() { - Log.i("[Phone State Listener] Unregistering phone state listener") - telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE) - } - - override fun isInCall(): Boolean { - return gsmCallActive - } -} diff --git a/app/src/main/java/org/linphone/compatibility/TelephonyListener.kt b/app/src/main/java/org/linphone/compatibility/TelephonyListener.kt deleted file mode 100644 index df96c50f6..000000000 --- a/app/src/main/java/org/linphone/compatibility/TelephonyListener.kt +++ /dev/null @@ -1,78 +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 . - */ -package org.linphone.compatibility - -import android.annotation.TargetApi -import android.os.Handler -import android.os.Looper -import android.telephony.TelephonyCallback -import android.telephony.TelephonyManager -import java.util.concurrent.Executor -import org.linphone.core.tools.Log - -@TargetApi(31) -class TelephonyListener(private val telephonyManager: TelephonyManager) : PhoneStateInterface { - private var gsmCallActive = false - - private fun runOnUiThreadExecutor(): Executor { - val handler = Handler(Looper.getMainLooper()) - return Executor { - handler.post(it) - } - } - - inner class TelephonyListener : TelephonyCallback(), TelephonyCallback.CallStateListener { - override fun onCallStateChanged(state: Int) { - gsmCallActive = when (state) { - TelephonyManager.CALL_STATE_OFFHOOK -> { - Log.i("[Context] Phone state is off hook") - true - } - TelephonyManager.CALL_STATE_RINGING -> { - Log.i("[Context] Phone state is ringing") - true - } - TelephonyManager.CALL_STATE_IDLE -> { - Log.i("[Context] Phone state is idle") - false - } - else -> { - Log.w("[Context] Phone state is unexpected: $state") - false - } - } - } - } - private val telephonyListener = TelephonyListener() - - init { - Log.i("[Telephony Listener] Registering telephony callback") - telephonyManager.registerTelephonyCallback(runOnUiThreadExecutor(), telephonyListener) - } - - override fun destroy() { - Log.i("[Telephony Listener] Unregistering telephony callback") - telephonyManager.unregisterTelephonyCallback(telephonyListener) - } - - override fun isInCall(): Boolean { - return gsmCallActive - } -} diff --git a/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt b/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt deleted file mode 100644 index a914c537d..000000000 --- a/app/src/main/java/org/linphone/compatibility/XiaomiCompatibility.kt +++ /dev/null @@ -1,128 +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 . - */ -package org.linphone.compatibility - -import android.annotation.TargetApi -import android.app.* -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import androidx.core.app.NotificationCompat -import androidx.core.content.ContextCompat -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.contact.getThumbnailUri -import org.linphone.core.Call -import org.linphone.core.Friend -import org.linphone.core.tools.Log -import org.linphone.notifications.Notifiable -import org.linphone.notifications.NotificationsManager -import org.linphone.utils.ImageUtils -import org.linphone.utils.LinphoneUtils - -@TargetApi(26) -class XiaomiCompatibility { - companion object { - fun createIncomingCallNotification( - context: Context, - call: Call, - notifiable: Notifiable, - pendingIntent: PendingIntent, - notificationsManager: NotificationsManager - ): Notification { - val contact: Friend? - val roundPicture: Bitmap? - val displayName: String - val address: String - val info: String - - val remoteContact = call.remoteContact - val conferenceAddress = if (remoteContact != null) { - coreContext.core.interpretUrl( - remoteContact, - false - ) - } else { - null - } - val conferenceInfo = if (conferenceAddress != null) { - coreContext.core.findConferenceInformationFromUri( - conferenceAddress - ) - } else { - null - } - if (conferenceInfo == null) { - Log.i( - "[Notifications Manager] No conference info found for remote contact address $remoteContact" - ) - contact = coreContext.contactsManager.findContactByAddress(call.remoteAddress) - roundPicture = - ImageUtils.getRoundBitmapFromUri(context, contact?.getThumbnailUri()) - displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress) - address = LinphoneUtils.getDisplayableAddress(call.remoteAddress) - info = context.getString(R.string.incoming_call_notification_title) - } else { - contact = null - displayName = conferenceInfo.subject ?: context.getString(R.string.conference) - address = LinphoneUtils.getDisplayableAddress(conferenceInfo.organizer) - roundPicture = coreContext.contactsManager.groupBitmap - info = context.getString(R.string.incoming_group_call_notification_title) - Log.i( - "[Notifications Manager] Displaying incoming group call notification with subject $displayName and remote contact address $remoteContact" - ) - } - - val builder = NotificationCompat.Builder( - context, - context.getString(R.string.notification_channel_incoming_call_id) - ) - .addPerson(notificationsManager.getPerson(contact, displayName, roundPicture)) - .setSmallIcon(R.drawable.topbar_call_notification) - .setLargeIcon( - roundPicture ?: BitmapFactory.decodeResource( - context.resources, - R.drawable.voip_single_contact_avatar_alt - ) - ) - .setContentTitle(displayName) - .setContentText(address) - .setSubText(info) - .setCategory(NotificationCompat.CATEGORY_CALL) - .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setWhen(System.currentTimeMillis()) - .setAutoCancel(false) - .setShowWhen(true) - .setOngoing(true) - .setColor(ContextCompat.getColor(context, R.color.primary_color)) - .setFullScreenIntent(pendingIntent, true) - .addAction(notificationsManager.getCallDeclineAction(notifiable)) - .addAction(notificationsManager.getCallAnswerAction(notifiable)) - - if (!corePreferences.preventInterfaceFromShowingUp) { - builder.setContentIntent(pendingIntent) - } - - return builder.build() - } - } -} diff --git a/app/src/main/java/org/linphone/contact/ContactAvatarGenerator.kt b/app/src/main/java/org/linphone/contact/ContactAvatarGenerator.kt deleted file mode 100644 index 2cb3b17b9..000000000 --- a/app/src/main/java/org/linphone/contact/ContactAvatarGenerator.kt +++ /dev/null @@ -1,113 +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 . - */ -package org.linphone.contact - -import android.content.Context -import android.graphics.* -import android.graphics.drawable.BitmapDrawable -import android.text.TextPaint -import android.util.TypedValue -import androidx.core.content.ContextCompat -import org.linphone.R -import org.linphone.utils.AppUtils - -class ContactAvatarGenerator(private val context: Context) { - private var textSize: Float - private var textColor: Int - private var avatarSize: Int - private var name = " " - private var backgroundColor: Int - - init { - val theme = context.theme - - val backgroundColorTypedValue = TypedValue() - theme.resolveAttribute(R.attr.primaryTextColor, backgroundColorTypedValue, true) - backgroundColor = ContextCompat.getColor(context, backgroundColorTypedValue.resourceId) - - val textColorTypedValue = TypedValue() - theme.resolveAttribute(R.attr.secondaryTextColor, textColorTypedValue, true) - textColor = ContextCompat.getColor(context, textColorTypedValue.resourceId) - - textSize = AppUtils.getDimension(R.dimen.contact_avatar_text_size) - - avatarSize = AppUtils.getDimension(R.dimen.contact_avatar_size).toInt() - } - - fun setTextSize(size: Float) = apply { - textSize = size - } - - fun setTextColorResource(resource: Int) = apply { - textColor = ContextCompat.getColor(context, resource) - } - - fun setAvatarSize(size: Int) = apply { - avatarSize = size - } - - fun setLabel(label: String) = apply { - name = label - } - - fun setBackgroundColorAttribute(attribute: Int) = apply { - val theme = context.theme - val backgroundColorTypedValue = TypedValue() - theme.resolveAttribute(attribute, backgroundColorTypedValue, true) - backgroundColor = ContextCompat.getColor(context, backgroundColorTypedValue.resourceId) - } - - fun build(): BitmapDrawable { - val label = AppUtils.getInitials(name) - val textPainter = getTextPainter() - val painter = getPainter() - - val bitmap = Bitmap.createBitmap(avatarSize, avatarSize, Bitmap.Config.ARGB_8888) - val canvas = Canvas(bitmap) - val areaRect = Rect(0, 0, avatarSize, avatarSize) - val bounds = RectF(areaRect) - bounds.right = textPainter.measureText(label, 0, label.length) - bounds.bottom = textPainter.descent() - textPainter.ascent() - bounds.left += (areaRect.width() - bounds.right) / 2.0f - bounds.top += (areaRect.height() - bounds.bottom) / 2.0f - - val halfSize = (avatarSize / 2).toFloat() - canvas.drawCircle(halfSize, halfSize, halfSize, painter) - canvas.drawText(label, bounds.left, bounds.top - textPainter.ascent(), textPainter) - - return BitmapDrawable(context.resources, bitmap) - } - - private fun getTextPainter(): TextPaint { - val textPainter = TextPaint() - textPainter.isAntiAlias = true - textPainter.textSize = textSize - textPainter.color = textColor - textPainter.typeface = Typeface.create(Typeface.DEFAULT, Typeface.BOLD) - return textPainter - } - - private fun getPainter(): Paint { - val painter = Paint() - painter.isAntiAlias = true - painter.color = backgroundColor - return painter - } -} diff --git a/app/src/main/java/org/linphone/contact/ContactDataInterface.kt b/app/src/main/java/org/linphone/contact/ContactDataInterface.kt deleted file mode 100644 index 2402d92bd..000000000 --- a/app/src/main/java/org/linphone/contact/ContactDataInterface.kt +++ /dev/null @@ -1,102 +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 . - */ -package org.linphone.contact - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.CoroutineScope -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.main.viewmodels.MessageNotifierViewModel -import org.linphone.core.Address -import org.linphone.core.ChatRoom.SecurityLevel -import org.linphone.core.ConsolidatedPresence -import org.linphone.core.Friend -import org.linphone.utils.LinphoneUtils - -interface ContactDataInterface { - val contact: MutableLiveData - - val displayName: MutableLiveData - - val securityLevel: MutableLiveData - - val showGroupChatAvatar: Boolean - get() = false - - val presenceStatus: MutableLiveData - - val coroutineScope: CoroutineScope -} - -open class GenericContactData(private val sipAddress: Address) : ContactDataInterface { - final override val contact: MutableLiveData = MutableLiveData() - final override val displayName: MutableLiveData = MutableLiveData() - final override val securityLevel: MutableLiveData = MutableLiveData() - final override val presenceStatus: MutableLiveData = MutableLiveData() - final override val coroutineScope: CoroutineScope = coreContext.coroutineScope - - init { - securityLevel.value = SecurityLevel.ClearText - presenceStatus.value = ConsolidatedPresence.Offline - contactLookup() - } - - open fun destroy() { - } - - protected fun contactLookup() { - displayName.value = LinphoneUtils.getDisplayName(sipAddress) - - val friend = coreContext.contactsManager.findContactByAddress(sipAddress) - if (friend != null) { - contact.value = friend!! - presenceStatus.value = friend.consolidatedPresence - friend.addListener { - presenceStatus.value = it.consolidatedPresence - } - } - } -} - -abstract class GenericContactViewModel(private val sipAddress: Address) : MessageNotifierViewModel(), ContactDataInterface { - final override val contact: MutableLiveData = MutableLiveData() - final override val displayName: MutableLiveData = MutableLiveData() - final override val securityLevel: MutableLiveData = MutableLiveData() - final override val presenceStatus: MutableLiveData = MutableLiveData() - final override val coroutineScope: CoroutineScope = viewModelScope - - init { - securityLevel.value = SecurityLevel.ClearText - presenceStatus.value = ConsolidatedPresence.Offline - contactLookup() - } - - private fun contactLookup() { - displayName.value = LinphoneUtils.getDisplayName(sipAddress) - val friend = coreContext.contactsManager.findContactByAddress(sipAddress) - if (friend != null) { - contact.value = friend!! - presenceStatus.value = friend.consolidatedPresence - friend.addListener { - presenceStatus.value = it.consolidatedPresence - } - } - } -} diff --git a/app/src/main/java/org/linphone/contact/ContactLoader.kt b/app/src/main/java/org/linphone/contact/ContactLoader.kt deleted file mode 100644 index 7e752cb02..000000000 --- a/app/src/main/java/org/linphone/contact/ContactLoader.kt +++ /dev/null @@ -1,344 +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 . - */ -package org.linphone.contact - -import android.content.ContentUris -import android.database.Cursor -import android.database.StaleDataException -import android.net.Uri -import android.os.Bundle -import android.provider.ContactsContract -import android.util.Patterns -import androidx.lifecycle.lifecycleScope -import androidx.loader.app.LoaderManager -import androidx.loader.content.CursorLoader -import androidx.loader.content.Loader -import java.lang.Exception -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils -import org.linphone.utils.PhoneNumberUtils - -class ContactLoader : LoaderManager.LoaderCallbacks { - companion object { - val projection = arrayOf( - ContactsContract.Data.CONTACT_ID, - ContactsContract.Contacts.DISPLAY_NAME_PRIMARY, - ContactsContract.Data.MIMETYPE, - ContactsContract.Contacts.STARRED, - ContactsContract.Contacts.LOOKUP_KEY, - "data1", // ContactsContract.CommonDataKinds.Phone.NUMBER, ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS, ContactsContract.CommonDataKinds.Organization.COMPANY - ContactsContract.CommonDataKinds.Phone.TYPE, - ContactsContract.CommonDataKinds.Phone.LABEL, - ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER - ) - } - - override fun onCreateLoader(id: Int, args: Bundle?): Loader { - val lastFetch = coreContext.contactsManager.latestContactFetch - Log.i( - "[Contacts Loader] Loader created, ${if (lastFetch.isEmpty()) "first fetch" else "last fetch happened at [$lastFetch]"}" - ) - coreContext.contactsManager.fetchInProgress.value = true - - val mimeType = ContactsContract.Data.MIMETYPE - val mimeSelection = "$mimeType = ? OR $mimeType = ? OR $mimeType = ? OR $mimeType = ?" - - val selection = if (corePreferences.fetchContactsFromDefaultDirectory) { - ContactsContract.Data.IN_DEFAULT_DIRECTORY + " == 1 AND ($mimeSelection)" - } else { - mimeSelection - } - - val linphoneMime = AppUtils.getString(R.string.linphone_address_mime_type) - val selectionArgs = arrayOf( - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, - ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, - linphoneMime, - ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE - ) - - return CursorLoader( - coreContext.context, - ContactsContract.Data.CONTENT_URI, - projection, - selection, - selectionArgs, - ContactsContract.Data.CONTACT_ID + " ASC" - ) - } - - override fun onLoadFinished(loader: Loader, cursor: Cursor?) { - if (cursor == null) { - Log.e("[Contacts Loader] Cursor is null!") - return - } - Log.i("[Contacts Loader] Load finished, found ${cursor.count} entries in cursor") - - val core = coreContext.core - val linphoneMime = loader.context.getString(R.string.linphone_address_mime_type) - val preferNormalizedPhoneNumber = corePreferences.preferNormalizedPhoneNumbersFromAddressBook - - if (core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off) { - Log.w("[Contacts Loader] Core is being stopped or already destroyed, abort") - return - } - - coreContext.lifecycleScope.launch { - val friends = HashMap() - - withContext(Dispatchers.IO) { - try { - // Cursor can be null now that we are on a different dispatcher according to Crashlytics - val friendsPhoneNumbers = arrayListOf() - val friendsAddresses = arrayListOf
() - var previousId = "" - while (cursor != null && !cursor.isClosed && cursor.moveToNext()) { - try { - val id: String = - cursor.getString( - cursor.getColumnIndexOrThrow(ContactsContract.Data.CONTACT_ID) - ) - val mime: String? = - cursor.getString( - cursor.getColumnIndexOrThrow(ContactsContract.Data.MIMETYPE) - ) - - if (previousId.isEmpty() || previousId != id) { - friendsPhoneNumbers.clear() - friendsAddresses.clear() - previousId = id - } - - val friend = friends[id] ?: core.createFriend() - friend.refKey = id - if (friend.name.isNullOrEmpty()) { - val displayName: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.Data.DISPLAY_NAME_PRIMARY - ) - ) - friend.name = displayName - - friend.photo = Uri.withAppendedPath( - ContentUris.withAppendedId( - ContactsContract.Contacts.CONTENT_URI, - id.toLong() - ), - ContactsContract.Contacts.Photo.CONTENT_DIRECTORY - ).toString() - - val starred = - cursor.getInt( - cursor.getColumnIndexOrThrow( - ContactsContract.Contacts.STARRED - ) - ) == 1 - friend.starred = starred - val lookupKey = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.Contacts.LOOKUP_KEY - ) - ) - friend.nativeUri = - "${ContactsContract.Contacts.CONTENT_LOOKUP_URI}/$lookupKey" - - // Disable short term presence - friend.isSubscribesEnabled = false - friend.incSubscribePolicy = SubscribePolicy.SPDeny - } - - when (mime) { - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE -> { - val data1: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.Phone.NUMBER - ) - ) - val data2: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.Phone.TYPE - ) - ) - val data3: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.Phone.LABEL - ) - ) - val data4: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER - ) - ) - - val label = - PhoneNumberUtils.addressBookLabelTypeToVcardParamString( - data2?.toInt() - ?: ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM, - data3 - ) - - val number = - if (preferNormalizedPhoneNumber || - data1.isNullOrEmpty() || - !Patterns.PHONE.matcher(data1).matches() - ) { - data4 ?: data1 - } else { - data1 - } - - if (number != null) { - if ( - friendsPhoneNumbers.find { - PhoneNumberUtils.arePhoneNumberWeakEqual( - it, - number - ) - } == null - ) { - val phoneNumber = Factory.instance() - .createFriendPhoneNumber(number, label) - friend.addPhoneNumberWithLabel(phoneNumber) - friendsPhoneNumbers.add(number) - } - } - } - linphoneMime, ContactsContract.CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE -> { - val sipAddress: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.SipAddress.SIP_ADDRESS - ) - ) - if (sipAddress != null) { - val address = core.interpretUrl(sipAddress, false) - if (address != null && - friendsAddresses.find { - it.weakEqual(address) - } == null - ) { - friend.addAddress(address) - friendsAddresses.add(address) - } - } - } - ContactsContract.CommonDataKinds.Organization.CONTENT_ITEM_TYPE -> { - val organization: String? = - cursor.getString( - cursor.getColumnIndexOrThrow( - ContactsContract.CommonDataKinds.Organization.COMPANY - ) - ) - if (organization != null) { - friend.organization = organization - } - } - // Our API not being thread safe this causes crashes sometimes given the Play Store reports - // So these values will be fetched at the only moment they are required: contact edition - /*ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE -> { - if (data2 != null && data3 != null) { - val vCard = friend.vcard - vCard?.givenName = data2 - vCard?.familyName = data3 - } - }*/ - } - - friends[id] = friend - } catch (e: Exception) { - Log.e("[Contacts Loader] Exception: $e") - } - } - - withContext(Dispatchers.Main) { - if (core.globalState == GlobalState.Shutdown || core.globalState == GlobalState.Off) { - Log.w( - "[Contacts Loader] Core is being stopped or already destroyed, abort" - ) - } else { - Log.i("[Contacts Loader] ${friends.size} friends created") - val contactId = coreContext.contactsManager.contactIdToWatchFor - if (contactId.isNotEmpty()) { - val friend = friends[contactId] - Log.i( - "[Contacts Loader] Manager was asked to monitor contact id $contactId" - ) - if (friend != null) { - Log.i( - "[Contacts Loader] Found new contact matching id $contactId, notifying listeners" - ) - coreContext.contactsManager.notifyListeners(friend) - } - } - - val fl = core.defaultFriendList ?: core.createFriendList() - for (friend in fl.friends) { - fl.removeFriend(friend) - } - - if (fl != core.defaultFriendList) core.addFriendList(fl) - - val friendsList = friends.values - for (friend in friendsList) { - fl.addLocalFriend(friend) - } - friends.clear() - Log.i("[Contacts Loader] Friends added") - - // Only update subscriptions when default account is registered or anytime if it isn't the first contacts fetch - if (core.defaultAccount?.state == RegistrationState.Ok || coreContext.contactsManager.latestContactFetch.isNotEmpty()) { - Log.i("[Contacts Loader] Updating friend list [$fl] subscriptions") - fl.updateSubscriptions() - } - - coreContext.contactsManager.fetchFinished() - } - } - } catch (sde: StaleDataException) { - Log.e("[Contacts Loader] State Data Exception: $sde") - } catch (ise: IllegalStateException) { - Log.e("[Contacts Loader] Illegal State Exception: $ise") - } catch (e: Exception) { - Log.e("[Contacts Loader] Exception: $e") - } finally { - cancel() - } - } - } - } - - override fun onLoaderReset(loader: Loader) { - Log.i("[Contacts Loader] Loader reset") - } -} diff --git a/app/src/main/java/org/linphone/contact/ContactSelectionData.kt b/app/src/main/java/org/linphone/contact/ContactSelectionData.kt deleted file mode 100644 index 94be145f5..000000000 --- a/app/src/main/java/org/linphone/contact/ContactSelectionData.kt +++ /dev/null @@ -1,105 +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 . - */ -package org.linphone.contact - -import androidx.lifecycle.MutableLiveData -import kotlinx.coroutines.CoroutineScope -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.* -import org.linphone.utils.LinphoneUtils - -class ContactSelectionData(private val searchResult: SearchResult) : ContactDataInterface { - override val contact: MutableLiveData = MutableLiveData() - override val displayName: MutableLiveData = MutableLiveData() - override val securityLevel: MutableLiveData = MutableLiveData() - override val presenceStatus: MutableLiveData = MutableLiveData() - override val coroutineScope: CoroutineScope = coreContext.coroutineScope - - val isDisabled: MutableLiveData by lazy { - MutableLiveData() - } - - val isSelected: MutableLiveData by lazy { - MutableLiveData() - } - - val isLinphoneUser: Boolean by lazy { - searchResult.friend?.getPresenceModelForUriOrTel( - searchResult.phoneNumber ?: searchResult.address?.asStringUriOnly() ?: "" - )?.basicStatus == PresenceBasicStatus.Open - } - - val sipUri: String by lazy { - searchResult.phoneNumber ?: LinphoneUtils.getDisplayableAddress(searchResult.address) - } - - val address: Address? by lazy { - searchResult.address - } - - val hasLimeX3DHCapability: Boolean - get() = LinphoneUtils.isEndToEndEncryptedChatAvailable() && searchResult.hasCapability( - Friend.Capability.LimeX3Dh - ) - - init { - isDisabled.value = false - isSelected.value = false - presenceStatus.value = ConsolidatedPresence.Offline - searchMatchingContact() - } - - private fun searchMatchingContact() { - val friend = searchResult.friend - if (friend != null) { - contact.value = friend!! - displayName.value = friend.name - presenceStatus.value = friend.consolidatedPresence - friend.addListener { - presenceStatus.value = it.consolidatedPresence - } - } else { - val address = searchResult.address - if (address != null) { - val found = coreContext.contactsManager.findContactByAddress(address) - if (found != null) { - contact.value = found!! - presenceStatus.value = found.consolidatedPresence - found.addListener { - presenceStatus.value = it.consolidatedPresence - } - } - displayName.value = LinphoneUtils.getDisplayName(address) - } else if (searchResult.phoneNumber != null) { - val found = coreContext.contactsManager.findContactByPhoneNumber( - searchResult.phoneNumber.orEmpty() - ) - if (found != null) { - contact.value = found!! - presenceStatus.value = found.consolidatedPresence - found.addListener { - presenceStatus.value = it.consolidatedPresence - } - } - displayName.value = searchResult.phoneNumber.orEmpty() - } - } - } -} diff --git a/app/src/main/java/org/linphone/contact/ContactsManager.kt b/app/src/main/java/org/linphone/contact/ContactsManager.kt deleted file mode 100644 index 07aafe7e8..000000000 --- a/app/src/main/java/org/linphone/contact/ContactsManager.kt +++ /dev/null @@ -1,465 +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 . - */ -package org.linphone.contact - -import android.accounts.Account -import android.accounts.AccountManager -import android.accounts.AuthenticatorDescription -import android.content.ContentResolver -import android.content.ContentUris -import android.content.Context -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.graphics.drawable.Drawable -import android.net.Uri -import android.provider.ContactsContract -import android.util.Patterns -import androidx.core.app.Person -import androidx.core.graphics.drawable.IconCompat -import androidx.lifecycle.MutableLiveData -import java.io.IOException -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.ImageUtils -import org.linphone.utils.LinphoneUtils -import org.linphone.utils.PermissionHelper -import org.linphone.utils.TimestampUtils - -interface ContactsUpdatedListener { - fun onContactsUpdated() - - fun onContactUpdated(friend: Friend) -} - -open class ContactsUpdatedListenerStub : ContactsUpdatedListener { - override fun onContactsUpdated() {} - - override fun onContactUpdated(friend: Friend) {} -} - -class ContactsManager(private val context: Context) { - val magicSearch: MagicSearch by lazy { - val magicSearch = coreContext.core.createMagicSearch() - magicSearch.limitedSearch = false - magicSearch - } - - var latestContactFetch: String = "" - - val fetchInProgress = MutableLiveData() - - var contactIdToWatchFor: String = "" - - val contactAvatar: IconCompat - val groupAvatar: IconCompat - val groupBitmap: Bitmap - - private val localFriends = arrayListOf() - - private val contactsUpdatedListeners = ArrayList() - - private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() { - @Synchronized - override fun onPresenceReceived(list: FriendList, friends: Array) { - Log.i("[Contacts Manager] Presence received") - for (friend in friends) { - refreshContactOnPresenceReceived(friend) - } - Log.i("[Contacts Manager] Contacts refreshed due to presence received") - notifyListeners() - Log.i("[Contacts Manager] Presence notified to all listeners") - } - } - - private val coreListener: CoreListenerStub = object : CoreListenerStub() { - override fun onFriendListCreated(core: Core, friendList: FriendList) { - friendList.addListener(friendListListener) - } - - override fun onFriendListRemoved(core: Core, friendList: FriendList) { - friendList.removeListener(friendListListener) - } - } - - init { - initSyncAccount() - - contactAvatar = IconCompat.createWithResource( - context, - R.drawable.voip_single_contact_avatar_alt - ) - groupAvatar = IconCompat.createWithResource( - context, - R.drawable.voip_multiple_contacts_avatar_alt - ) - groupBitmap = BitmapFactory.decodeResource( - context.resources, - R.drawable.voip_multiple_contacts_avatar_alt - ) - - val core = coreContext.core - core.addListener(coreListener) - for (list in core.friendsLists) { - list.addListener(friendListListener) - } - Log.i("[Contacts Manager] Created") - } - - fun shouldDisplaySipContactsList(): Boolean { - return coreContext.core.defaultAccount?.params?.identityAddress?.domain == corePreferences.defaultDomain - } - - fun fetchFinished() { - Log.i("[Contacts Manager] Contacts loader have finished") - latestContactFetch = TimestampUtils.timeToString(System.currentTimeMillis(), false) - updateLocalContacts() - fetchInProgress.value = false - notifyListeners() - } - - @Synchronized - fun updateLocalContacts() { - Log.i("[Contacts Manager] Updating local contact(s)") - localFriends.clear() - - for (account in coreContext.core.accountList) { - val friend = coreContext.core.createFriend() - friend.name = LinphoneUtils.getDisplayName(account.params.identityAddress) - - val address = account.params.identityAddress ?: continue - friend.address = address - - val pictureUri = corePreferences.defaultAccountAvatarPath - if (pictureUri != null) { - val parsedUri = if (pictureUri.startsWith("/")) "file:$pictureUri" else pictureUri - Log.i("[Contacts Manager] Found local picture URI: $parsedUri") - friend.photo = parsedUri - } - - Log.i( - "[Contacts Manager] Local contact created for account [${address.asString()}] and picture [${friend.photo}]" - ) - localFriends.add(friend) - } - } - - @Synchronized - fun getMePerson(localAddress: Address): Person { - val friend = localFriends.find { localFriend -> - localFriend.addresses.find { address -> - address.weakEqual(localAddress) - } != null - } - return friend?.getPerson() - ?: Person.Builder().setName(LinphoneUtils.getDisplayName(localAddress)).build() - } - - @Synchronized - fun getAndroidContactIdFromUri(uri: Uri): String? { - val projection = arrayOf(ContactsContract.Data.CONTACT_ID) - val cursor = context.contentResolver.query(uri, projection, null, null, null) - if (cursor?.moveToFirst() == true) { - val nameColumnIndex = cursor.getColumnIndex(ContactsContract.Data.CONTACT_ID) - val id = cursor.getString(nameColumnIndex) - cursor.close() - return id - } - return null - } - - @Synchronized - fun findContactById(id: String): Friend? { - return coreContext.core.defaultFriendList?.findFriendByRefKey(id) - } - - @Synchronized - fun findContactByPhoneNumber(number: String): Friend? { - return coreContext.core.findFriendByPhoneNumber(number) - } - - @Synchronized - fun findContactByAddress(address: Address): Friend? { - for (friend in localFriends) { - if (friend.address?.weakEqual(address) == true) { - return friend - } - } - - val friend = coreContext.core.findFriend(address) - if (friend != null) return friend - - val username = address.username - if (username != null && Patterns.PHONE.matcher(username).matches()) { - return findContactByPhoneNumber(username) - } - - return null - } - - @Synchronized - fun isAddressMyself(address: Address): Boolean { - for (friend in localFriends) { - if (friend.address?.weakEqual(address) == true) { - return true - } - } - return false - } - - @Synchronized - fun addListener(listener: ContactsUpdatedListener) { - contactsUpdatedListeners.add(listener) - } - - @Synchronized - fun removeListener(listener: ContactsUpdatedListener) { - contactsUpdatedListeners.remove(listener) - } - - @Synchronized - fun notifyListeners() { - val list = contactsUpdatedListeners.toMutableList() - for (listener in list) { - listener.onContactsUpdated() - } - } - - @Synchronized - fun notifyListeners(friend: Friend) { - val list = contactsUpdatedListeners.toMutableList() - for (listener in list) { - listener.onContactUpdated(friend) - } - } - - @Synchronized - fun destroy() { - val core = coreContext.core - for (list in core.friendsLists) list.removeListener(friendListListener) - core.removeListener(coreListener) - } - - private fun initSyncAccount() { - val accountManager = context.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager - val accounts = accountManager.getAccountsByType( - context.getString(R.string.sync_account_type) - ) - if (accounts.isEmpty()) { - val newAccount = Account( - context.getString(R.string.sync_account_name), - context.getString( - R.string.sync_account_type - ) - ) - try { - accountManager.addAccountExplicitly(newAccount, null, null) - Log.i("[Contacts Manager] Contact account added") - } catch (e: Exception) { - Log.e("[Contacts Manager] Couldn't initialize sync account: $e") - } - } else { - for (account in accounts) { - Log.i( - "[Contacts Manager] Found account with name [${account.name}] and type [${account.type}]" - ) - } - } - } - - fun getAvailableSyncAccounts(): List> { - val accountManager = context.getSystemService(Context.ACCOUNT_SERVICE) as AccountManager - val packageManager = context.packageManager - val syncAdapters = ContentResolver.getSyncAdapterTypes() - val authenticators: Array = accountManager.authenticatorTypes - val available = arrayListOf>() - - for (syncAdapter in syncAdapters) { - if (syncAdapter.authority == "com.android.contacts" && syncAdapter.isUserVisible) { - if (syncAdapter.supportsUploading() || syncAdapter.accountType == context.getString( - R.string.sync_account_type - ) - ) { - Log.i( - "[Contacts Manager] Found sync adapter for com.android.contacts authority: ${syncAdapter.accountType}" - ) - val accounts = accountManager.getAccountsByType(syncAdapter.accountType) - for (account in accounts) { - Log.i( - "[Contacts Manager] Found account for account type ${syncAdapter.accountType}: ${account.name}" - ) - for (authenticator in authenticators) { - if (authenticator.type == account.type) { - val drawable = packageManager.getDrawable( - authenticator.packageName, - authenticator.smallIconId, - null - ) - val triple = Triple(account.name, account.type, drawable) - available.add(triple) - } - } - } - } - } - } - - return available - } - - @Synchronized - private fun refreshContactOnPresenceReceived(friend: Friend) { - Log.d( - "[Contacts Manager] Received presence information for contact [${friend.name}]: [${friend.consolidatedPresence}]" - ) - if (corePreferences.storePresenceInNativeContact && PermissionHelper.get().hasWriteContactsPermission()) { - if (friend.refKey != null) { - Log.i("[Contacts Manager] Storing presence in native contact ${friend.refKey}") - storePresenceInNativeContact(friend) - } - } - notifyListeners(friend) - } - - private fun storePresenceInNativeContact(friend: Friend) { - val contactEditor = NativeContactEditor(friend) - for (phoneNumber in friend.phoneNumbers) { - val sipAddress = friend.getContactForPhoneNumberOrAddress(phoneNumber) - if (sipAddress != null) { - Log.d( - "[Contacts Manager] Found presence information to store in native contact $friend under Linphone sync account" - ) - contactEditor.setPresenceInformation( - phoneNumber, - sipAddress - ) - } - } - contactEditor.commit() - } - - fun createFriendFromSearchResult(searchResult: SearchResult): Friend { - val searchResultFriend = searchResult.friend - if (searchResultFriend != null) return searchResultFriend - - val friend = coreContext.core.createFriend() - - val address = searchResult.address - if (address != null) { - friend.address = address - } - - val number = searchResult.phoneNumber - if (number != null) { - friend.addPhoneNumber(number) - - if (address != null && address.username == number) { - friend.removeAddress(address) - } - } - - return friend - } -} - -fun Friend.getContactForPhoneNumberOrAddress(value: String): String? { - val presenceModel = getPresenceModelForUriOrTel(value) - if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return presenceModel.contact - return null -} - -fun Friend.hasLongTermPresence(): Boolean { - for (address in addresses) { - val presenceModel = getPresenceModelForUriOrTel(address.asStringUriOnly()) - if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true - } - for (number in phoneNumbers) { - val presenceModel = getPresenceModelForUriOrTel(number) - if (presenceModel != null && presenceModel.basicStatus == PresenceBasicStatus.Open) return true - } - return false -} - -fun Friend.getThumbnailUri(): Uri? { - return getPictureUri(true) -} - -fun Friend.getPictureUri(thumbnailPreferred: Boolean = false): Uri? { - val refKey = refKey - if (refKey != null) { - try { - val lookupUri = ContentUris.withAppendedId( - ContactsContract.Contacts.CONTENT_URI, - refKey.toLong() - ) - - if (!thumbnailPreferred) { - val pictureUri = Uri.withAppendedPath( - lookupUri, - ContactsContract.Contacts.Photo.DISPLAY_PHOTO - ) - // Check that the URI points to a real file - val contentResolver = coreContext.context.contentResolver - try { - val fd = contentResolver.openAssetFileDescriptor(pictureUri, "r") - if (fd != null) { - fd.close() - return pictureUri - } - } catch (_: IOException) { } - } - - // Fallback to thumbnail if high res picture isn't available - return Uri.withAppendedPath( - lookupUri, - ContactsContract.Contacts.Photo.CONTENT_DIRECTORY - ) - } catch (_: Exception) { } - } else if (photo != null) { - try { - return Uri.parse(photo) - } catch (_: Exception) { } - } - return null -} - -fun Friend.getPerson(): Person { - val personBuilder = Person.Builder().setName(name) - - val bm: Bitmap? = - ImageUtils.getRoundBitmapFromUri( - coreContext.context, - getThumbnailUri() - ) - personBuilder.setIcon( - if (bm == null) { - coreContext.contactsManager.contactAvatar - } else { - IconCompat.createWithAdaptiveBitmap(bm) - } - ) - - personBuilder.setKey(refKey) - personBuilder.setUri(nativeUri) - personBuilder.setImportant(starred) - return personBuilder.build() -} diff --git a/app/src/main/java/org/linphone/contact/ContactsSelectionAdapter.kt b/app/src/main/java/org/linphone/contact/ContactsSelectionAdapter.kt deleted file mode 100644 index 76cffca9d..000000000 --- a/app/src/main/java/org/linphone/contact/ContactsSelectionAdapter.kt +++ /dev/null @@ -1,160 +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 . - */ -package org.linphone.contact - -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.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.core.Address -import org.linphone.core.Friend.Capability -import org.linphone.core.SearchResult -import org.linphone.databinding.ContactSelectionCellBinding -import org.linphone.utils.Event - -class ContactsSelectionAdapter( - private val viewLifecycleOwner: LifecycleOwner -) : ListAdapter(SearchResultDiffCallback()) { - val selectedContact = MutableLiveData>() - - private val selection = MutableLiveData>() - - private val requireGroupChatCapability = MutableLiveData() - private var requireLimeCapability = MutableLiveData() - - init { - requireGroupChatCapability.value = false - requireLimeCapability.value = false - } - - fun updateSelectedAddresses(selectedAddresses: List
) { - selection.value = selectedAddresses - } - - fun setLimeCapabilityRequired(enabled: Boolean) { - requireLimeCapability.value = enabled - } - - fun setGroupChatCapabilityRequired(enabled: Boolean) { - requireGroupChatCapability.value = enabled - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val binding: ContactSelectionCellBinding = DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.contact_selection_cell, - parent, - false - ) - return ViewHolder(binding) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - (holder as ViewHolder).bind(getItem(position)) - } - - inner class ViewHolder( - private val binding: ContactSelectionCellBinding - ) : RecyclerView.ViewHolder(binding.root) { - fun bind(searchResult: SearchResult) { - with(binding) { - val searchResultViewModel = ContactSelectionData(searchResult) - data = searchResultViewModel - - lifecycleOwner = viewLifecycleOwner - - requireLimeCapability.observe(viewLifecycleOwner) { - updateSecurity(searchResult, searchResultViewModel) - } - requireGroupChatCapability.observe(viewLifecycleOwner) { - updateSecurity(searchResult, searchResultViewModel) - } - - selection.observe(viewLifecycleOwner) { selectedAddresses -> - val selected = selectedAddresses.find { address -> - val searchAddress = searchResult.address - if (searchAddress != null) address.weakEqual(searchAddress) else false - } - searchResultViewModel.isSelected.value = selected != null - } - - setClickListener { - selectedContact.value = Event(searchResult) - } - - executePendingBindings() - } - } - - private fun updateSecurity( - searchResult: SearchResult, - viewModel: ContactSelectionData - ) { - val securityEnabled = requireLimeCapability.value ?: false - val groupCapabilityRequired = requireGroupChatCapability.value ?: false - val searchAddress = searchResult.address - val isMyself = securityEnabled && searchAddress != null && coreContext.core.defaultAccount?.params?.identityAddress?.weakEqual( - searchAddress - ) ?: false - val limeCheck = !securityEnabled || ( - securityEnabled && searchResult.hasCapability( - Capability.LimeX3Dh - ) - ) - val groupCheck = !groupCapabilityRequired || ( - groupCapabilityRequired && searchResult.hasCapability( - Capability.GroupChat - ) - ) - val disabled = if (searchResult.friend != null) !limeCheck || !groupCheck || isMyself else false // Generated entry from search filter - - viewModel.isDisabled.value = disabled - - if (disabled && viewModel.isSelected.value == true) { - // Remove item from selection if both selected and disabled - selectedContact.postValue(Event(searchResult)) - } - } - } -} - -private class SearchResultDiffCallback : DiffUtil.ItemCallback() { - override fun areItemsTheSame( - oldItem: SearchResult, - newItem: SearchResult - ): Boolean { - val oldAddress = oldItem.address - val newAddress = newItem.address - return if (oldAddress != null && newAddress != null) oldAddress.weakEqual(newAddress) else false - } - - override fun areContentsTheSame( - oldItem: SearchResult, - newItem: SearchResult - ): Boolean { - return newItem.friend != null - } -} diff --git a/app/src/main/java/org/linphone/contact/ContactsSelectionViewModel.kt b/app/src/main/java/org/linphone/contact/ContactsSelectionViewModel.kt deleted file mode 100644 index 46f47dbc6..000000000 --- a/app/src/main/java/org/linphone/contact/ContactsSelectionViewModel.kt +++ /dev/null @@ -1,177 +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 . - */ -package org.linphone.contact - -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.* -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.activities.main.viewmodels.MessageNotifierViewModel -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.Event - -open class ContactsSelectionViewModel : MessageNotifierViewModel() { - val contactsList = MutableLiveData>() - - val sipContactsSelected = MutableLiveData() - - val selectedAddresses = MutableLiveData>() - - val filter = MutableLiveData() - private var previousFilter = "NotSet" - - val fetchInProgress = MutableLiveData() - private var searchResultsPending: Boolean = false - private var fastFetchJob: Job? = null - - val moreResultsAvailableEvent: MutableLiveData> by lazy { - MutableLiveData>() - } - - private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() { - override fun onContactsUpdated() { - Log.i("[Contacts Selection] Contacts have changed") - applyFilter() - } - } - - private val magicSearchListener = object : MagicSearchListenerStub() { - override fun onSearchResultsReceived(magicSearch: MagicSearch) { - searchResultsPending = false - processMagicSearchResults(magicSearch.lastSearch) - fetchInProgress.value = false - } - - override fun onLdapHaveMoreResults(magicSearch: MagicSearch, ldap: Ldap) { - moreResultsAvailableEvent.value = Event(true) - } - } - - init { - sipContactsSelected.value = coreContext.contactsManager.shouldDisplaySipContactsList() - - selectedAddresses.value = arrayListOf() - - coreContext.contactsManager.addListener(contactsUpdatedListener) - coreContext.contactsManager.magicSearch.addListener(magicSearchListener) - } - - override fun onCleared() { - coreContext.contactsManager.magicSearch.removeListener(magicSearchListener) - coreContext.contactsManager.removeListener(contactsUpdatedListener) - - super.onCleared() - } - - fun applyFilter() { - val filterValue = filter.value.orEmpty().trim() - - if (previousFilter.isNotEmpty() && ( - previousFilter.length > filterValue.length || - (previousFilter.length == filterValue.length && previousFilter != filterValue) - ) - ) { - coreContext.contactsManager.magicSearch.resetSearchCache() - } - previousFilter = filterValue - - val domain = if (sipContactsSelected.value == true) coreContext.core.defaultAccount?.params?.domain ?: "" else "" - searchResultsPending = true - fastFetchJob?.cancel() - coreContext.contactsManager.magicSearch.getContactsListAsync( - filterValue, - domain, - MagicSearch.Source.All.toInt(), - MagicSearch.Aggregation.None - ) - - val spinnerDelay = corePreferences.delayBeforeShowingContactsSearchSpinner.toLong() - fastFetchJob = viewModelScope.launch { - withContext(Dispatchers.IO) { - delay(spinnerDelay) - withContext(Dispatchers.Main) { - if (searchResultsPending) { - fetchInProgress.value = true - } - } - } - } - } - - fun clearFilter() { - filter.value = "" - } - - fun toggleSelectionForSearchResult(searchResult: SearchResult) { - val address = searchResult.address - if (address != null) { - toggleSelectionForAddress(address) - } - } - - fun toggleSelectionForAddress(address: Address) { - val list = arrayListOf
() - list.addAll(selectedAddresses.value.orEmpty()) - - val found = list.find { - it.weakEqual(address) - } - - if (found != null) { - list.remove(found) - } else { - val contact = coreContext.contactsManager.findContactByAddress(address) - if (contact != null) { - val clone = address.clone() - clone.displayName = contact.name - list.add(clone) - } else { - list.add(address) - } - } - - selectedAddresses.value = list - } - - private fun processMagicSearchResults(results: Array) { - Log.i("[Contacts Selection] Processing ${results.size} results") - val list = arrayListOf() - for (result in results) { - if (result.sourceFlags == MagicSearch.Source.Request.toInt()) { - val address = result.address - if (address != null) { - val found = list.find { - it.address?.weakEqual(address) ?: false - } - if (found != null) { - Log.i( - "[Contacts Selection] User-input is already present in search results, skipping request" - ) - continue - } - } - } - list.add(result) - } - contactsList.postValue(list) - } -} diff --git a/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt b/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt deleted file mode 100644 index 3cc3cf4b0..000000000 --- a/app/src/main/java/org/linphone/contact/DummyAuthenticationService.kt +++ /dev/null @@ -1,96 +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 . - */ - -package org.linphone.contact - -import android.accounts.AbstractAccountAuthenticator -import android.accounts.Account -import android.accounts.AccountAuthenticatorResponse -import android.app.Service -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.os.IBinder - -// Below classes are required to be able to create our DummySyncService... -internal class DummyAuthenticator(context: Context) : AbstractAccountAuthenticator(context) { - override fun getAuthTokenLabel(authTokenType: String?): String { - throw UnsupportedOperationException() - } - - override fun confirmCredentials( - response: AccountAuthenticatorResponse?, - account: Account?, - options: Bundle? - ): Bundle? = null - - override fun updateCredentials( - response: AccountAuthenticatorResponse?, - account: Account?, - authTokenType: String?, - options: Bundle? - ): Bundle { - throw UnsupportedOperationException() - } - - override fun getAuthToken( - response: AccountAuthenticatorResponse?, - account: Account?, - authTokenType: String?, - options: Bundle? - ): Bundle { - throw UnsupportedOperationException() - } - - override fun hasFeatures( - response: AccountAuthenticatorResponse?, - account: Account?, - features: Array? - ): Bundle { - throw UnsupportedOperationException() - } - - override fun editProperties( - response: AccountAuthenticatorResponse?, - accountType: String? - ): Bundle { - throw UnsupportedOperationException() - } - - override fun addAccount( - response: AccountAuthenticatorResponse?, - accountType: String?, - authTokenType: String?, - requiredFeatures: Array?, - options: Bundle? - ): Bundle? = null -} - -class DummyAuthenticationService : Service() { - private lateinit var authenticator: DummyAuthenticator - - override fun onCreate() { - authenticator = DummyAuthenticator(this) - } - - override fun onBind(intent: Intent): IBinder { - return authenticator.iBinder - } -} diff --git a/app/src/main/java/org/linphone/contact/DummySyncService.kt b/app/src/main/java/org/linphone/contact/DummySyncService.kt deleted file mode 100644 index 3aca7e36a..000000000 --- a/app/src/main/java/org/linphone/contact/DummySyncService.kt +++ /dev/null @@ -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 . - */ -package org.linphone.contact - -import android.accounts.Account -import android.app.Service -import android.content.* -import android.os.Bundle -import android.os.IBinder - -// Below classes are required to be able to use our own contact MIME type entry... -class DummySyncAdapter(context: Context, autoInit: Boolean) : AbstractThreadedSyncAdapter( - context, - autoInit -) { - override fun onPerformSync( - account: Account?, - extras: Bundle?, - authority: String?, - provider: ContentProviderClient?, - syncResult: SyncResult? - ) { } -} - -class DummySyncService : Service() { - companion object { - private val syncAdapterLock = Any() - private var syncAdapter: DummySyncAdapter? = null - } - - override fun onCreate() { - synchronized(syncAdapterLock) { - if (syncAdapter == null) { - syncAdapter = DummySyncAdapter(applicationContext, true) - } - } - } - - override fun onBind(intent: Intent?): IBinder? { - return syncAdapter?.syncAdapterBinder - } -} diff --git a/app/src/main/java/org/linphone/contact/NativeContactEditor.kt b/app/src/main/java/org/linphone/contact/NativeContactEditor.kt deleted file mode 100644 index 1e96818fb..000000000 --- a/app/src/main/java/org/linphone/contact/NativeContactEditor.kt +++ /dev/null @@ -1,594 +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 . - */ -package org.linphone.contact - -import android.content.ContentProviderOperation -import android.content.ContentUris -import android.net.Uri -import android.provider.ContactsContract -import android.provider.ContactsContract.CommonDataKinds -import android.provider.ContactsContract.RawContacts -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.main.contact.data.NumberOrAddressEditorData -import org.linphone.core.Friend -import org.linphone.core.tools.Log -import org.linphone.utils.AppUtils -import org.linphone.utils.PermissionHelper - -class NativeContactEditor(val friend: Friend) { - companion object { - fun createAndroidContact(accountName: String?, accountType: String?): Long { - Log.i("[Native Contact Editor] Using sync account $accountName with type $accountType") - - val changes = arrayListOf() - changes.add( - ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) - .withValue(RawContacts.ACCOUNT_NAME, accountName) - .withValue(RawContacts.ACCOUNT_TYPE, accountType) - .build() - ) - val contentResolver = coreContext.context.contentResolver - val results = contentResolver.applyBatch(ContactsContract.AUTHORITY, changes) - for (result in results) { - val uri = result.uri - Log.i("[Native Contact Editor] Contact creation result is $uri") - if (uri != null) { - try { - val cursor = contentResolver.query( - uri, - arrayOf(RawContacts.CONTACT_ID), - null, - null, - null - ) - if (cursor != null) { - cursor.moveToNext() - val contactId: Long = cursor.getLong(0) - Log.i("[Native Contact Editor] New contact id is $contactId") - cursor.close() - return contactId - } - } catch (e: Exception) { - Log.e("[Native Contact Editor] Failed to get cursor: $e") - } - } - } - - return 0 - } - } - - private val changes = arrayListOf() - private val selection = - "${ContactsContract.Data.CONTACT_ID} =? AND ${ContactsContract.Data.MIMETYPE} =?" - private val phoneNumberSelection = - "$selection AND (${CommonDataKinds.Phone.NUMBER}=? OR ${CommonDataKinds.Phone.NORMALIZED_NUMBER}=?)" - private val presenceUpdateSelection = - "${ContactsContract.Data.CONTACT_ID} =? AND ${ContactsContract.Data.MIMETYPE} =? AND data3=?" - private val contactUri = ContactsContract.Data.CONTENT_URI - - private var rawId: String? = null - private var syncAccountRawId: String? = null - private var pictureByteArray: ByteArray? = null - - init { - val contentResolver = coreContext.context.contentResolver - val cursor = contentResolver.query( - RawContacts.CONTENT_URI, - arrayOf(RawContacts._ID), - "${RawContacts.CONTACT_ID} =?", - arrayOf(friend.refKey), - null - ) - if (cursor?.moveToFirst() == true) { - do { - if (rawId == null) { - try { - rawId = cursor.getString(cursor.getColumnIndexOrThrow(RawContacts._ID)) - Log.d( - "[Native Contact Editor] Found raw id $rawId for native contact with id ${friend.refKey}" - ) - } catch (iae: IllegalArgumentException) { - Log.e("[Native Contact Editor] Exception: $iae") - } - } - } while (cursor.moveToNext() && rawId == null) - } - cursor?.close() - } - - fun setFirstAndLastNames(firstName: String, lastName: String): NativeContactEditor { - if (firstName == friend.vcard?.givenName && lastName == friend.vcard?.familyName) { - Log.w("[Native Contact Editor] First & last names haven't changed") - return this - } - - val builder = if (friend.vcard?.givenName == null && friend.vcard?.familyName == null) { - // Probably a contact creation - ContentProviderOperation.newInsert(contactUri) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId) - } else { - ContentProviderOperation.newUpdate(contactUri) - .withSelection( - selection, - arrayOf(friend.refKey, CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE) - ) - } - - builder.withValue( - ContactsContract.Data.MIMETYPE, - CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE - ) - .withValue( - CommonDataKinds.StructuredName.GIVEN_NAME, - firstName - ) - .withValue( - CommonDataKinds.StructuredName.FAMILY_NAME, - lastName - ) - addChanges(builder.build()) - return this - } - - fun setOrganization(value: String): NativeContactEditor { - val previousValue = friend.organization.orEmpty() - if (value == previousValue) { - Log.d("[Native Contact Editor] Organization hasn't changed") - return this - } - - val builder = if (previousValue.isNotEmpty()) { - ContentProviderOperation.newUpdate(contactUri) - .withSelection( - "$selection AND ${CommonDataKinds.Organization.COMPANY} =?", - arrayOf( - friend.refKey, - CommonDataKinds.Organization.CONTENT_ITEM_TYPE, - previousValue - ) - ) - } else { - ContentProviderOperation.newInsert(contactUri) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId) - } - - builder.withValue( - ContactsContract.Data.MIMETYPE, - CommonDataKinds.Organization.CONTENT_ITEM_TYPE - ) - .withValue( - CommonDataKinds.Organization.COMPANY, - value - ) - - addChanges(builder.build()) - return this - } - - fun setPhoneNumbers(value: List): NativeContactEditor { - var addCount = 0 - var removeCount = 0 - var editCount = 0 - - for (phoneNumber in value) { - when { - phoneNumber.currentValue.isEmpty() -> { - // New phone number to add - val number = phoneNumber.newValue.value.orEmpty() - if (number.isNotEmpty()) { - addCount++ - addPhoneNumber(number) - } - } - phoneNumber.toRemove.value == true -> { - // Existing number to remove - removeCount++ - removePhoneNumber(phoneNumber.currentValue) - } - phoneNumber.currentValue != phoneNumber.newValue.value -> { - // Existing number to update - val number = phoneNumber.newValue.value.orEmpty() - if (number.isNotEmpty()) { - editCount++ - updatePhoneNumber(phoneNumber.currentValue, number) - } - } - } - } - - Log.i( - "[Native Contact Editor] $addCount numbers added, $removeCount numbers removed and $editCount numbers updated" - ) - return this - } - - fun setSipAddresses(value: List): NativeContactEditor { - var addCount = 0 - var removeCount = 0 - var editCount = 0 - - for (sipAddress in value) { - when { - sipAddress.currentValue.isEmpty() -> { - // New address to add - val address = sipAddress.newValue.value.orEmpty() - if (address.isNotEmpty()) { - addCount++ - addSipAddress(address) - } - } - sipAddress.toRemove.value == true -> { - // Existing address to remove - removeCount++ - removeLinphoneOrSipAddress(sipAddress.currentValue) - } - sipAddress.currentValue != sipAddress.newValue.value -> { - // Existing address to update - val address = sipAddress.newValue.value.orEmpty() - if (address.isNotEmpty()) { - editCount++ - updateLinphoneOrSipAddress(sipAddress.currentValue, address) - } - } - } - } - - Log.i( - "[Native Contact Editor] $addCount addresses added, $removeCount addresses removed and $editCount addresses updated" - ) - return this - } - - fun setPicture(value: ByteArray?): NativeContactEditor { - pictureByteArray = value - if (value != null) Log.i("[Native Contact Editor] Adding operation: picture set/update") - return this - } - - fun setPresenceInformation(phoneNumber: String, sipAddress: String): NativeContactEditor { - if (syncAccountRawId == null) { - val contentResolver = coreContext.context.contentResolver - val cursor = contentResolver.query( - RawContacts.CONTENT_URI, - arrayOf(RawContacts._ID, RawContacts.ACCOUNT_TYPE), - "${RawContacts.CONTACT_ID} =?", - arrayOf(friend.refKey), - null - ) - if (cursor?.moveToFirst() == true) { - do { - try { - val accountType = - cursor.getString(cursor.getColumnIndexOrThrow(RawContacts.ACCOUNT_TYPE)) - if (accountType == AppUtils.getString(R.string.sync_account_type) && syncAccountRawId == null) { - syncAccountRawId = - cursor.getString(cursor.getColumnIndexOrThrow(RawContacts._ID)) - Log.d( - "[Native Contact Editor] Found linphone raw id $syncAccountRawId for native contact with id ${friend.refKey}" - ) - } - } catch (iae: IllegalArgumentException) { - Log.e("[Native Contact Editor] Exception: $iae") - } - } while (cursor.moveToNext() && syncAccountRawId == null) - } - cursor?.close() - } - - if (syncAccountRawId == null) { - Log.w("[Native Contact Editor] Linphone raw id not found") - val insert = ContentProviderOperation.newInsert(RawContacts.CONTENT_URI) - .withValue(RawContacts.ACCOUNT_NAME, AppUtils.getString(R.string.sync_account_name)) - .withValue(RawContacts.ACCOUNT_TYPE, AppUtils.getString(R.string.sync_account_type)) - .withValue(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT) - .build() - addChanges(insert) - val update = - ContentProviderOperation.newUpdate( - ContactsContract.AggregationExceptions.CONTENT_URI - ) - .withValue( - ContactsContract.AggregationExceptions.TYPE, - ContactsContract.AggregationExceptions.TYPE_KEEP_TOGETHER - ) - .withValue(ContactsContract.AggregationExceptions.RAW_CONTACT_ID1, rawId) - .withValueBackReference( - ContactsContract.AggregationExceptions.RAW_CONTACT_ID2, - 0 - ) - .build() - addChanges(update) - commit(true) - } - - if (syncAccountRawId == null) { - Log.e( - "[Native Contact Editor] Can't add presence to contact in Linphone sync account, no raw id" - ) - return this - } - - Log.d("[Native Contact Editor] Trying to add presence information to contact") - setPresenceLinphoneSipAddressForPhoneNumber(sipAddress, phoneNumber) - return this - } - - fun commit(updateSyncAccountRawId: Boolean = false) { - if (PermissionHelper.get().hasWriteContactsPermission()) { - try { - if (changes.isNotEmpty()) { - val contentResolver = coreContext.context.contentResolver - val results = contentResolver.applyBatch(ContactsContract.AUTHORITY, changes) - for (result in results) { - val uri = result.uri - Log.i("[Native Contact Editor] Result is $uri") - if (uri != null && updateSyncAccountRawId && syncAccountRawId == null) { - syncAccountRawId = ContentUris.parseId(uri).toString() - Log.i( - "[Native Contact Editor] Sync account raw id is $syncAccountRawId" - ) - } - } - } - if (pictureByteArray != null) { - updatePicture() - } - } catch (e: Exception) { - Log.e("[Native Contact Editor] Exception raised while applying changes: $e") - } - } else { - Log.e("[Native Contact Editor] WRITE_CONTACTS permission isn't granted!") - } - changes.clear() - } - - private fun addChanges(operation: ContentProviderOperation) { - Log.i("[Native Contact Editor] Adding operation: $operation") - changes.add(operation) - } - - private fun addPhoneNumber(phoneNumber: String) { - val insert = ContentProviderOperation.newInsert(contactUri) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId) - .withValue( - ContactsContract.Data.MIMETYPE, - CommonDataKinds.Phone.CONTENT_ITEM_TYPE - ) - .withValue(CommonDataKinds.Phone.NUMBER, phoneNumber) - .withValue( - CommonDataKinds.Phone.TYPE, - CommonDataKinds.Phone.TYPE_MOBILE - ) - .build() - addChanges(insert) - } - - private fun updatePhoneNumber(currentValue: String, phoneNumber: String) { - val update = ContentProviderOperation.newUpdate(contactUri) - .withSelection( - phoneNumberSelection, - arrayOf( - friend.refKey, - CommonDataKinds.Phone.CONTENT_ITEM_TYPE, - currentValue, - currentValue - ) - ) - .withValue(ContactsContract.Data.MIMETYPE, CommonDataKinds.Phone.CONTENT_ITEM_TYPE) - .withValue(CommonDataKinds.Phone.NUMBER, phoneNumber) - .withValue( - CommonDataKinds.Phone.TYPE, - CommonDataKinds.Phone.TYPE_MOBILE - ) - .build() - addChanges(update) - } - - private fun removePhoneNumber(phoneNumber: String) { - val delete = ContentProviderOperation.newDelete(contactUri) - .withSelection( - phoneNumberSelection, - arrayOf( - friend.refKey, - CommonDataKinds.Phone.CONTENT_ITEM_TYPE, - phoneNumber, - phoneNumber - ) - ) - .build() - addChanges(delete) - } - - private fun addSipAddress(address: String) { - val insert = ContentProviderOperation.newInsert(contactUri) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, rawId) - .withValue( - ContactsContract.Data.MIMETYPE, - CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE - ) - .withValue("data1", address) // value - .build() - addChanges(insert) - } - - private fun updateLinphoneOrSipAddress(currentValue: String, sipAddress: String) { - val updateLegacy = ContentProviderOperation.newUpdate(contactUri) - .withSelection( - "${ContactsContract.Data.CONTACT_ID} =? AND ${ContactsContract.Data.MIMETYPE} =? AND data1=?", - arrayOf( - friend.refKey, - AppUtils.getString(R.string.linphone_address_mime_type), - currentValue - ) - ) - .withValue("data1", sipAddress) // value - .withValue("data2", AppUtils.getString(R.string.app_name)) // summary - .withValue("data3", sipAddress) // detail - .build() - - val update = ContentProviderOperation.newUpdate(contactUri) - .withSelection( - "${ContactsContract.Data.CONTACT_ID} =? AND ${ContactsContract.Data.MIMETYPE} =? AND data1=?", - arrayOf( - friend.refKey, - CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, - currentValue - ) - ) - .withValue("data1", sipAddress) // value - .build() - - addChanges(updateLegacy) - addChanges(update) - } - - private fun removeLinphoneOrSipAddress(sipAddress: String) { - val delete = ContentProviderOperation.newDelete(contactUri) - .withSelection( - "${ContactsContract.Data.CONTACT_ID} =? AND (${ContactsContract.Data.MIMETYPE} =? OR ${ContactsContract.Data.MIMETYPE} =?) AND data1=?", - arrayOf( - friend.refKey, - CommonDataKinds.SipAddress.CONTENT_ITEM_TYPE, - AppUtils.getString(R.string.linphone_address_mime_type), - sipAddress - ) - ) - .build() - addChanges(delete) - } - - private fun setPresenceLinphoneSipAddressForPhoneNumber(sipAddress: String, phoneNumber: String) { - val contentResolver = coreContext.context.contentResolver - val cursor = contentResolver.query( - ContactsContract.Data.CONTENT_URI, - arrayOf("data1"), - "${ContactsContract.Data.RAW_CONTACT_ID} = ? AND ${ContactsContract.Data.MIMETYPE} = ? AND data3 = ?", - arrayOf( - syncAccountRawId, - AppUtils.getString(R.string.linphone_address_mime_type), - phoneNumber - ), - null - ) - val count = cursor?.count ?: 0 - val data1 = if (count > 0) { - if (cursor?.moveToFirst() == true) { - try { - cursor.getString(cursor.getColumnIndexOrThrow("data1")) - } catch (iae: IllegalArgumentException) { - Log.e("[Native Contact Editor] Exception: $iae") - } - } else { - null - } - } else { - null - } - cursor?.close() - - val address = if (sipAddress.endsWith(";user=phone")) { - sipAddress.substring(0, sipAddress.length - ";user=phone".length) - } else { - sipAddress - } - - if (count == 0) { - Log.i( - "[Native Contact Editor] No existing presence information found for this phone number ($phoneNumber) & SIP address ($address), let's add it" - ) - addPresenceLinphoneSipAddressForPhoneNumber(address, phoneNumber) - } else { - if (data1 != null && data1 == address) { - Log.d( - "[Native Contact Editor] There is already an entry for this phone number and SIP address, skipping" - ) - } else { - Log.w( - "[Native Contact Editor] There is already an entry for this phone number ($phoneNumber) but not for the same SIP address ($data1 != $address)" - ) - updatePresenceLinphoneSipAddressForPhoneNumber(address, phoneNumber) - } - } - } - - private fun addPresenceLinphoneSipAddressForPhoneNumber(address: String, detail: String) { - val insert = ContentProviderOperation.newInsert(contactUri) - .withValue(ContactsContract.Data.RAW_CONTACT_ID, syncAccountRawId) - .withValue( - ContactsContract.Data.MIMETYPE, - AppUtils.getString(R.string.linphone_address_mime_type) - ) - .withValue("data1", address) // value - .withValue("data2", AppUtils.getString(R.string.app_name)) // summary - .withValue("data3", detail) // detail - .build() - addChanges(insert) - } - - private fun updatePresenceLinphoneSipAddressForPhoneNumber( - sipAddress: String, - phoneNumber: String - ) { - val update = ContentProviderOperation.newUpdate(contactUri) - .withSelection( - presenceUpdateSelection, - arrayOf( - friend.refKey, - AppUtils.getString(R.string.linphone_address_mime_type), - phoneNumber - ) - ) - .withValue( - ContactsContract.Data.MIMETYPE, - AppUtils.getString(R.string.linphone_address_mime_type) - ) - .withValue("data1", sipAddress) // value - .withValue("data2", AppUtils.getString(R.string.app_name)) // summary - .withValue("data3", phoneNumber) // detail - .build() - addChanges(update) - } - - private fun updatePicture() { - val value = pictureByteArray - val id = rawId - if (value == null || id == null) return - - try { - val uri = Uri.withAppendedPath( - ContentUris.withAppendedId(RawContacts.CONTENT_URI, id.toLong()), - RawContacts.DisplayPhoto.CONTENT_DIRECTORY - ) - val contentResolver = coreContext.context.contentResolver - val assetFileDescriptor = contentResolver.openAssetFileDescriptor(uri, "rw") - val outputStream = assetFileDescriptor?.createOutputStream() - outputStream?.write(value) - outputStream?.close() - assetFileDescriptor?.close() - Log.i("[Native Contact Editor] Picture updated") - } catch (e: Exception) { - Log.e("[Native Contact Editor] Failed to update picture, raised exception: $e") - } - - pictureByteArray = null - } -} diff --git a/app/src/main/java/org/linphone/core/BootReceiver.kt b/app/src/main/java/org/linphone/core/BootReceiver.kt deleted file mode 100644 index 951eacde0..000000000 --- a/app/src/main/java/org/linphone/core/BootReceiver.kt +++ /dev/null @@ -1,60 +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 . - */ -package org.linphone.core - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import androidx.core.app.NotificationManagerCompat -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.compatibility.Compatibility -import org.linphone.core.tools.Log - -class BootReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - if (intent.action.equals(Intent.ACTION_BOOT_COMPLETED, ignoreCase = true)) { - val autoStart = corePreferences.autoStart - Log.i("[Boot Receiver] Device is starting, autoStart is $autoStart") - if (autoStart) { - startService(context) - } - } else if (intent.action.equals(Intent.ACTION_MY_PACKAGE_REPLACED, ignoreCase = true)) { - val autoStart = corePreferences.autoStart - Log.i("[Boot Receiver] App has been updated, autoStart is $autoStart") - if (autoStart) { - startService(context) - } - } - } - - private fun startService(context: Context) { - val serviceChannel = context.getString(R.string.notification_channel_service_id) - val notificationManager = NotificationManagerCompat.from(context) - if (Compatibility.getChannelImportance(notificationManager, serviceChannel) == NotificationManagerCompat.IMPORTANCE_NONE) { - Log.w("[Boot Receiver] Service channel is disabled!") - return - } - - val serviceIntent = Intent(Intent.ACTION_MAIN).setClass(context, CoreService::class.java) - serviceIntent.putExtra("StartForeground", true) - Compatibility.startForegroundService(context, serviceIntent) - } -} diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt deleted file mode 100644 index 812642c77..000000000 --- a/app/src/main/java/org/linphone/core/CoreContext.kt +++ /dev/null @@ -1,1319 +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 . - */ -package org.linphone.core - -import android.app.Application -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.graphics.PixelFormat -import android.media.AudioDeviceCallback -import android.media.AudioDeviceInfo -import android.media.AudioManager -import android.os.Handler -import android.os.Looper -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyProperties -import android.telephony.TelephonyManager -import android.util.Base64 -import android.util.Pair -import android.view.* -import android.webkit.MimeTypeMap -import androidx.lifecycle.* -import androidx.loader.app.LoaderManager -import com.google.firebase.crashlytics.FirebaseCrashlytics -import java.io.File -import java.math.BigInteger -import java.nio.charset.StandardCharsets -import java.security.KeyStore -import java.security.MessageDigest -import java.text.Collator -import java.util.* -import javax.crypto.Cipher -import javax.crypto.KeyGenerator -import javax.crypto.SecretKey -import javax.crypto.spec.GCMParameterSpec -import kotlin.math.abs -import kotlinx.coroutines.* -import org.linphone.BuildConfig -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.compatibility.Compatibility -import org.linphone.compatibility.PhoneStateInterface -import org.linphone.contact.ContactLoader -import org.linphone.contact.ContactsManager -import org.linphone.contact.getContactForPhoneNumberOrAddress -import org.linphone.core.tools.Log -import org.linphone.mediastream.Version -import org.linphone.notifications.NotificationsManager -import org.linphone.telecom.TelecomHelper -import org.linphone.utils.* -import org.linphone.utils.Event - -class CoreContext( - val context: Context, - coreConfig: Config, - service: CoreService? = null, - useAutoStartDescription: Boolean = false -) : - LifecycleOwner, ViewModelStoreOwner { - - private val _lifecycleRegistry = LifecycleRegistry(this) - override val lifecycle: Lifecycle - get() = _lifecycleRegistry - - private val _viewModelStore = ViewModelStore() - override val viewModelStore: ViewModelStore - get() = _viewModelStore - - private val contactLoader = ContactLoader() - - private val collator: Collator = Collator.getInstance() - - var stopped = false - val core: Core - val handler: Handler = Handler(Looper.getMainLooper()) - - var screenWidth: Float = 0f - var screenHeight: Float = 0f - - val appVersion: String by lazy { - val appVersion = BuildConfig.VERSION_NAME - val appBranch = context.getString(R.string.linphone_app_branch) - val appBuildType = BuildConfig.BUILD_TYPE - "$appVersion ($appBranch, $appBuildType)" - } - - val sdkVersion: String by lazy { - val sdkVersion = context.getString(org.linphone.core.R.string.linphone_sdk_version) - val sdkBranch = context.getString(org.linphone.core.R.string.linphone_sdk_branch) - val sdkBuildType = org.linphone.core.BuildConfig.BUILD_TYPE - "$sdkVersion ($sdkBranch, $sdkBuildType)" - } - - val contactsManager: ContactsManager by lazy { - ContactsManager(context) - } - - val notificationsManager: NotificationsManager by lazy { - NotificationsManager(context) - } - - val callErrorMessageResourceId: MutableLiveData> by lazy { - MutableLiveData>() - } - - val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) - - private val loggingService = Factory.instance().loggingService - - private var overlayX = 0f - private var overlayY = 0f - private var callOverlay: View? = null - private var previousCallState = Call.State.Idle - private lateinit var phoneStateListener: PhoneStateInterface - - private val activityMonitor = ActivityMonitor() - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String) { - Log.i("[Context] Global state changed [$state]") - if (state == GlobalState.On) { - if (corePreferences.disableVideo) { - // if video has been disabled, don't forget to tell the Core to disable it as well - Log.w( - "[Context] Video has been disabled in app, disabling it as well in the Core" - ) - core.isVideoCaptureEnabled = false - core.isVideoDisplayEnabled = false - - val videoPolicy = core.videoActivationPolicy - videoPolicy.automaticallyInitiate = false - videoPolicy.automaticallyAccept = false - core.videoActivationPolicy = videoPolicy - } - - fetchContacts() - } - } - - override fun onAccountRegistrationStateChanged( - core: Core, - account: Account, - state: RegistrationState?, - message: String - ) { - Log.i( - "[Context] Account [${account.params.identityAddress?.asStringUriOnly()}] registration state changed [$state]" - ) - if (state == RegistrationState.Ok && account == core.defaultAccount) { - notificationsManager.stopForegroundNotificationIfPossible() - } - } - - override fun onPushNotificationReceived(core: Core, payload: String?) { - Log.i("[Context] Push notification received: $payload") - } - - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - Log.i("[Context] Call state changed [$state]") - if (state == Call.State.IncomingReceived || state == Call.State.IncomingEarlyMedia) { - if (declineCallDueToGsmActiveCall()) { - call.decline(Reason.Busy) - return - } - - // Starting SDK 24 (Android 7.0) we rely on the fullscreen intent of the call incoming notification - if (Version.sdkStrictlyBelow(Version.API24_NOUGAT_70)) { - onIncomingReceived() - } - - if (corePreferences.autoAnswerEnabled) { - val autoAnswerDelay = corePreferences.autoAnswerDelay - if (autoAnswerDelay == 0) { - Log.w("[Context] Auto answering call immediately") - answerCall(call) - } else { - Log.i( - "[Context] Scheduling auto answering in $autoAnswerDelay milliseconds" - ) - handler.postDelayed( - { - Log.w("[Context] Auto answering call") - answerCall(call) - }, - autoAnswerDelay.toLong() - ) - } - } - } else if (state == Call.State.OutgoingProgress) { - val conferenceInfo = core.findConferenceInformationFromUri(call.remoteAddress) - // Do not show outgoing call view for conference calls, wait for connected state - if (conferenceInfo == null) { - onOutgoingStarted() - } - - if (core.callsNb == 1 && corePreferences.routeAudioToBluetoothIfAvailable) { - AudioRouteUtils.routeAudioToBluetooth(call) - } - } else if (state == Call.State.Connected) { - onCallStarted() - } else if (state == Call.State.StreamsRunning) { - if (previousCallState == Call.State.Connected) { - // Do not automatically route audio to bluetooth after first call - if (core.callsNb == 1) { - // Only try to route bluetooth / headphone / headset when the call is in StreamsRunning for the first time - Log.i( - "[Context] First call going into StreamsRunning state for the first time, trying to route audio to headset or bluetooth if available" - ) - if (AudioRouteUtils.isHeadsetAudioRouteAvailable()) { - AudioRouteUtils.routeAudioToHeadset(call) - } else if (corePreferences.routeAudioToBluetoothIfAvailable && AudioRouteUtils.isBluetoothAudioRouteAvailable()) { - AudioRouteUtils.routeAudioToBluetooth(call) - } - } - - // Only start call recording when the call is in StreamsRunning for the first time - if (corePreferences.automaticallyStartCallRecording && !call.params.isRecording) { - if (call.conference == null) { // TODO: FIXME: We disabled conference recording for now - Log.i( - "[Context] We were asked to start the call recording automatically" - ) - call.startRecording() - } - } - } - } else if (state == Call.State.End || state == Call.State.Error || state == Call.State.Released) { - if (state == Call.State.Error) { - Log.w( - "[Context] Call error reason is ${call.errorInfo.protocolCode} / ${call.errorInfo.reason} / ${call.errorInfo.phrase}" - ) - val toastMessage = when (call.errorInfo.reason) { - Reason.Busy -> context.getString(R.string.call_error_user_busy) - Reason.IOError -> context.getString(R.string.call_error_io_error) - Reason.NotAcceptable -> context.getString( - R.string.call_error_incompatible_media_params - ) - Reason.NotFound -> context.getString(R.string.call_error_user_not_found) - Reason.ServerTimeout -> context.getString( - R.string.call_error_server_timeout - ) - Reason.TemporarilyUnavailable -> context.getString( - R.string.call_error_temporarily_unavailable - ) - else -> context.getString(R.string.call_error_generic).format( - "${call.errorInfo.protocolCode} / ${call.errorInfo.phrase}" - ) - } - callErrorMessageResourceId.value = Event(toastMessage) - } else if (state == Call.State.End && - call.dir == Call.Dir.Outgoing && - call.errorInfo.reason == Reason.Declined && - core.callsNb == 0 - ) { - Log.i("[Context] Call has been declined") - val toastMessage = context.getString(R.string.call_error_declined) - callErrorMessageResourceId.value = Event(toastMessage) - } - } - - previousCallState = state - } - - override fun onLastCallEnded(core: Core) { - Log.i("[Context] Last call has ended") - removeCallOverlay() - if (!core.isMicEnabled) { - Log.w("[Context] Mic was muted in Core, enabling it back for next call") - core.isMicEnabled = true - } - } - - override fun onMessagesReceived( - core: Core, - chatRoom: ChatRoom, - messages: Array - ) { - for (message in messages) { - exportFileInMessage(message) - } - } - } - - private val loggingServiceListener = object : LoggingServiceListenerStub() { - override fun onLogMessageWritten( - logService: LoggingService, - domain: String, - level: LogLevel, - message: String - ) { - if (corePreferences.logcatLogsOutput) { - when (level) { - LogLevel.Error -> android.util.Log.e(domain, message) - LogLevel.Warning -> android.util.Log.w(domain, message) - LogLevel.Message -> android.util.Log.i(domain, message) - LogLevel.Fatal -> android.util.Log.wtf(domain, message) - else -> android.util.Log.d(domain, message) - } - } - FirebaseCrashlytics.getInstance().log("[$domain] [${level.name}] $message") - } - } - - init { - if (context.resources.getBoolean(R.bool.crashlytics_enabled)) { - loggingService.addListener(loggingServiceListener) - Log.i("[Context] Crashlytics enabled, register logging service listener") - } - - _lifecycleRegistry.currentState = Lifecycle.State.INITIALIZED - - Log.i("=========================================") - Log.i("==== Linphone-android information dump ====") - Log.i("VERSION=${BuildConfig.VERSION_NAME} / ${BuildConfig.VERSION_CODE}") - Log.i("PACKAGE=${BuildConfig.APPLICATION_ID}") - Log.i("BUILD TYPE=${BuildConfig.BUILD_TYPE}") - Log.i("=========================================") - - if (service != null) { - Log.i("[Context] Starting foreground service") - notificationsManager.startForegroundToKeepAppAlive(service, useAutoStartDescription) - } - - core = Factory.instance().createCoreWithConfig(coreConfig, context) - - stopped = false - _lifecycleRegistry.currentState = Lifecycle.State.CREATED - - (context as Application).registerActivityLifecycleCallbacks(activityMonitor) - Log.i("[Context] Ready") - } - - private val audioDeviceCallback = object : AudioDeviceCallback() { - override fun onAudioDevicesAdded(addedDevices: Array?) { - if (!addedDevices.isNullOrEmpty()) { - Log.i("[Context] [${addedDevices.size}] new device(s) have been added") - core.reloadSoundDevices() - } - } - - override fun onAudioDevicesRemoved(removedDevices: Array?) { - if (!removedDevices.isNullOrEmpty()) { - Log.i("[Context] [${removedDevices.size}] existing device(s) have been removed") - core.reloadSoundDevices() - } - } - } - - fun start() { - Log.i("[Context] Starting") - - core.addListener(listener) - - // CoreContext listener must be added first! - if (Version.sdkAboveOrEqual(Version.API26_O_80) && corePreferences.useTelecomManager) { - if (Compatibility.hasTelecomManagerPermissions(context)) { - Log.i( - "[Context] Creating Telecom Helper, disabling audio focus requests in AudioHelper" - ) - core.config.setBool("audio", "android_disable_audio_focus_requests", true) - val telecomHelper = TelecomHelper.required(context) - Log.i( - "[Context] Telecom Helper created, account is ${if (telecomHelper.isAccountEnabled()) "enabled" else "disabled"}" - ) - } else { - Log.w("[Context] Can't create Telecom Helper, permissions have been revoked") - corePreferences.useTelecomManager = false - } - } - - configureCore() - - core.start() - _lifecycleRegistry.currentState = Lifecycle.State.STARTED - - initPhoneStateListener() - - notificationsManager.onCoreReady() - - collator.strength = Collator.NO_DECOMPOSITION - - if (corePreferences.vfsEnabled) { - val notClearedCount = FileUtils.countFilesInDirectory(corePreferences.vfsCachePath) - if (notClearedCount > 0) { - Log.w( - "[Context] [VFS] There are [$notClearedCount] plain files not cleared from previous app lifetime, removing them now" - ) - } - FileUtils.clearExistingPlainFiles() - } - - if (corePreferences.keepServiceAlive) { - Log.i("[Context] Background mode setting is enabled, starting Service") - notificationsManager.startForeground() - } - - _lifecycleRegistry.currentState = Lifecycle.State.RESUMED - - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - audioManager.registerAudioDeviceCallback(audioDeviceCallback, handler) - Log.i("[Context] Started") - } - - fun stop() { - Log.i("[Context] Stopping") - coroutineScope.cancel() - - if (::phoneStateListener.isInitialized) { - phoneStateListener.destroy() - } - notificationsManager.destroy() - contactsManager.destroy() - if (TelecomHelper.exists()) { - Log.i("[Context] Destroying telecom helper") - TelecomHelper.get().destroy() - TelecomHelper.destroy() - } - - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - audioManager.unregisterAudioDeviceCallback(audioDeviceCallback) - - core.stop() - core.removeListener(listener) - stopped = true - _lifecycleRegistry.currentState = Lifecycle.State.DESTROYED - loggingService.removeListener(loggingServiceListener) - - (context as Application).unregisterActivityLifecycleCallbacks(activityMonitor) - } - - fun onForeground() { - // We can't rely on defaultAccount?.params?.isPublishEnabled - // as it will be modified by the SDK when changing the presence status - if (corePreferences.publishPresence) { - Log.i("[Context] App is in foreground, PUBLISHING presence as Online") - core.consolidatedPresence = ConsolidatedPresence.Online - } - } - - fun onBackground() { - // We can't rely on defaultAccount?.params?.isPublishEnabled - // as it will be modified by the SDK when changing the presence status - if (corePreferences.publishPresence) { - Log.i("[Context] App is in background, un-PUBLISHING presence info") - // We don't use ConsolidatedPresence.Busy but Offline to do an unsubscribe, - // Flexisip will handle the Busy status depending on other devices - core.consolidatedPresence = ConsolidatedPresence.Offline - } - } - - private fun configureCore() { - Log.i("[Context] Configuring Core") - - core.staticPicture = corePreferences.staticPicturePath - - // Migration code - if (core.config.getBool("app", "incoming_call_vibration", true)) { - core.isVibrationOnIncomingCallEnabled = true - core.config.setBool("app", "incoming_call_vibration", false) - } - - if (core.config.getInt("misc", "conference_layout", 1) > 1) { - core.config.setInt("misc", "conference_layout", 1) - } - - // Now LIME server URL is set on accounts - val limeServerUrl = core.limeX3DhServerUrl.orEmpty() - if (limeServerUrl.isNotEmpty()) { - Log.w("[Context] Removing LIME X3DH server URL from Core config") - core.limeX3DhServerUrl = null - } - - // Disable Telecom Manager on Android < 10 to prevent crash due to OS bug in Android 9 - if (Version.sdkStrictlyBelow(Version.API29_ANDROID_10)) { - if (corePreferences.useTelecomManager) { - Log.w( - "[Context] Android < 10 detected, disabling telecom manager to prevent crash due to OS bug" - ) - } - corePreferences.useTelecomManager = false - corePreferences.manuallyDisabledTelecomManager = true - } - - initUserCertificates() - - computeUserAgent() - - val fiveTwoMigrationRequired = core.config.getBool("app", "migration_5.2_required", true) - if (fiveTwoMigrationRequired) { - Log.i( - "[Context] Starting migration of muted chat room from shared preferences to our SDK" - ) - val sharedPreferences: SharedPreferences = context.getSharedPreferences( - "notifications", - Context.MODE_PRIVATE - ) - val editor = sharedPreferences.edit() - - for (chatRoom in core.chatRooms) { - val id = LinphoneUtils.getChatRoomId(chatRoom) - if (sharedPreferences.getBoolean(id, false)) { - Log.i("[Context] Migrating muted flag for chat room [$id]") - chatRoom.muted = true - editor.remove(id) - } - } - - editor.apply() - core.config.setBool( - "app", - "migration_5.2_required", - false - ) - Log.i("[Context] Migration of muted chat room finished") - - core.setVideoCodecPriorityPolicy(CodecPriorityPolicy.Auto) - Log.i("[Context] Video codec priority policy updated to Auto") - } - - val fiveOneMigrationRequired = core.config.getBool("app", "migration_5.1_required", true) - if (fiveOneMigrationRequired) { - core.config.setBool( - "sip", - "update_presence_model_timestamp_before_publish_expires_refresh", - true - ) - } - - for (account in core.accountList) { - if (account.params.identityAddress?.domain == corePreferences.defaultDomain) { - var paramsChanged = false - val params = account.params.clone() - - if (fiveOneMigrationRequired) { - val newExpire = 31536000 // 1 year - if (account.params.expires != newExpire) { - Log.i( - "[Context] Updating expire on account ${params.identityAddress?.asString()} from ${account.params.expires} to newExpire" - ) - params.expires = newExpire - paramsChanged = true - } - - // Enable presence publish/subscribe for new feature - if (!account.params.isPublishEnabled) { - Log.i( - "[Context] Enabling presence publish on account ${params.identityAddress?.asString()}" - ) - params.isPublishEnabled = true - params.publishExpires = 120 - paramsChanged = true - } - } - - // Ensure conference factory URI is set on sip.linphone.org accounts - if (account.params.conferenceFactoryUri == null) { - val uri = corePreferences.conferenceServerUri - Log.i( - "[Context] Setting conference factory on account ${params.identityAddress?.asString()} to default value: $uri" - ) - params.conferenceFactoryUri = uri - paramsChanged = true - } - - // Ensure audio/video conference factory URI is set on sip.linphone.org accounts - if (account.params.audioVideoConferenceFactoryAddress == null) { - val uri = corePreferences.audioVideoConferenceServerUri - val address = core.interpretUrl(uri, false) - if (address != null) { - Log.i( - "[Context] Setting audio/video conference factory on account ${params.identityAddress?.asString()} to default value: $uri" - ) - params.audioVideoConferenceFactoryAddress = address - paramsChanged = true - } else { - Log.e("[Context] Failed to parse audio/video conference factory URI: $uri") - } - } - - // Enable Bundle mode by default - if (!account.params.isRtpBundleEnabled) { - Log.i( - "[Context] Enabling RTP bundle mode on account ${params.identityAddress?.asString()}" - ) - params.isRtpBundleEnabled = true - paramsChanged = true - } - - // Ensure we allow CPIM messages in basic chat rooms - if (!account.params.isCpimInBasicChatRoomEnabled) { - params.isCpimInBasicChatRoomEnabled = true - paramsChanged = true - Log.i( - "[Context] CPIM allowed in basic chat rooms for account ${params.identityAddress?.asString()}" - ) - } - - if (account.params.limeServerUrl.isNullOrEmpty()) { - if (limeServerUrl.isNotEmpty()) { - params.limeServerUrl = limeServerUrl - paramsChanged = true - Log.i( - "[Context] Moving Core's LIME X3DH server URL [$limeServerUrl] on account ${params.identityAddress?.asString()}" - ) - } else { - params.limeServerUrl = corePreferences.limeServerUrl - paramsChanged = true - Log.w( - "[Context] Linphone account [${params.identityAddress?.asString()}] didn't have a LIME X3DH server URL, setting one: ${corePreferences.limeServerUrl}" - ) - } - } - - if (paramsChanged) { - Log.i("[Context] Account params have been updated, apply changes") - account.params = params - } - } - } - core.config.setBool("app", "migration_5.1_required", false) - - Log.i("[Context] Core configured") - } - - private fun computeUserAgent() { - val deviceName: String = corePreferences.deviceName - val appName: String = context.resources.getString(R.string.user_agent_app_name) - val androidVersion = BuildConfig.VERSION_NAME - val userAgent = "$appName/$androidVersion ($deviceName) LinphoneSDK" - val sdkVersion = context.getString(org.linphone.core.R.string.linphone_sdk_version) - val sdkBranch = context.getString(org.linphone.core.R.string.linphone_sdk_branch) - val sdkUserAgent = "$sdkVersion ($sdkBranch)" - core.setUserAgent(userAgent, sdkUserAgent) - } - - private fun initUserCertificates() { - val userCertsPath = corePreferences.userCertificatesPath - val f = File(userCertsPath) - if (!f.exists()) { - if (!f.mkdir()) { - Log.e("[Context] $userCertsPath can't be created.") - } - } - core.userCertificatesPath = userCertsPath - } - - fun fetchContacts() { - if (corePreferences.enableNativeAddressBookIntegration) { - if (PermissionHelper.required(context).hasReadContactsPermission()) { - Log.i("[Context] Init contacts loader") - val manager = LoaderManager.getInstance(this@CoreContext) - manager.restartLoader(0, null, contactLoader) - } - } - } - - fun newAccountConfigured(isLinphoneAccount: Boolean) { - Log.i( - "[Context] A new ${if (isLinphoneAccount) AppUtils.getString(R.string.app_name) else "third-party"} account has been configured" - ) - - if (isLinphoneAccount) { - core.config.setString("sip", "rls_uri", corePreferences.defaultRlsUri) - val rlsAddress = core.interpretUrl(corePreferences.defaultRlsUri, false) - if (rlsAddress != null) { - for (friendList in core.friendsLists) { - friendList.rlsAddress = rlsAddress - } - } - if (core.mediaEncryption == MediaEncryption.None) { - Log.i("[Context] Enabling SRTP media encryption instead of None") - core.mediaEncryption = MediaEncryption.SRTP - } - } else { - Log.i("[Context] Background mode with foreground service automatically enabled") - corePreferences.keepServiceAlive = true - notificationsManager.startForeground() - } - - contactsManager.updateLocalContacts() - } - - /* Call related functions */ - - fun initPhoneStateListener() { - if (PermissionHelper.required(context).hasReadPhoneStatePermission()) { - try { - phoneStateListener = - Compatibility.createPhoneListener( - context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - ) - } catch (exception: SecurityException) { - val hasReadPhoneStatePermission = - PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission() - Log.e( - "[Context] Failed to create phone state listener: $exception, READ_PHONE_STATE permission status is $hasReadPhoneStatePermission" - ) - } - } else { - Log.w( - "[Context] Can't create phone state listener, READ_PHONE_STATE permission isn't granted" - ) - } - } - - fun declineCallDueToGsmActiveCall(): Boolean { - if (!corePreferences.useTelecomManager) { // Can't use the following call with Telecom Manager API as it will "fake" GSM calls - var gsmCallActive = false - if (::phoneStateListener.isInitialized) { - gsmCallActive = phoneStateListener.isInCall() - } - - if (gsmCallActive) { - Log.w("[Context] Refusing the call with reason busy because a GSM call is active") - return true - } - } else { - if (TelecomHelper.exists()) { - if (!TelecomHelper.get().isIncomingCallPermitted() || - TelecomHelper.get().isInManagedCall() - ) { - Log.w( - "[Context] Refusing the call with reason busy because Telecom Manager will reject the call" - ) - return true - } - } else { - Log.e("[Context] Telecom Manager singleton wasn't created!") - } - } - return false - } - - fun videoUpdateRequestTimedOut(call: Call) { - coroutineScope.launch { - Log.w("[Context] 30 seconds have passed, declining video request") - answerCallVideoUpdateRequest(call, false) - } - } - - fun answerCallVideoUpdateRequest(call: Call, accept: Boolean) { - val params = core.createCallParams(call) - - if (accept) { - params?.isVideoEnabled = true - core.isVideoCaptureEnabled = true - core.isVideoDisplayEnabled = true - } else { - params?.isVideoEnabled = false - } - - call.acceptUpdate(params) - } - - fun answerCall(call: Call) { - Log.i("[Context] Answering call $call") - val params = core.createCallParams(call) - if (params == null) { - Log.w("[Context] Answering call without params!") - call.accept() - return - } - - params.recordFile = LinphoneUtils.getRecordingFilePathForAddress(call.remoteAddress) - - if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) { - Log.w("[Context] Enabling low bandwidth mode!") - params.isLowBandwidthEnabled = true - } - - if (call.callLog.wasConference()) { - // Prevent incoming group call to start in audio only layout - // Do the same as the conference waiting room - params.isVideoEnabled = true - params.videoDirection = if (core.videoActivationPolicy.automaticallyInitiate) MediaDirection.SendRecv else MediaDirection.RecvOnly - Log.i( - "[Context] Enabling video on call params to prevent audio-only layout when answering" - ) - } - - call.acceptWithParams(params) - } - - fun declineCall(call: Call) { - val voiceMailUri = corePreferences.voiceMailUri - if (voiceMailUri != null && corePreferences.redirectDeclinedCallToVoiceMail) { - val voiceMailAddress = core.interpretUrl(voiceMailUri, false) - if (voiceMailAddress != null) { - Log.i("[Context] Redirecting call $call to voice mail URI: $voiceMailUri") - call.redirectTo(voiceMailAddress) - } - } else { - val reason = if (core.callsNb > 1) { - Reason.Busy - } else { - Reason.Declined - } - Log.i("[Context] Declining call [$call] with reason [$reason]") - call.decline(reason) - } - } - - fun terminateCall(call: Call) { - Log.i("[Context] Terminating call $call") - call.terminate() - } - - fun transferCallTo(addressToCall: String): Boolean { - val currentCall = core.currentCall ?: core.calls.firstOrNull() - if (currentCall == null) { - Log.e("[Context] Couldn't find a call to transfer") - } else { - val address = core.interpretUrl(addressToCall, LinphoneUtils.applyInternationalPrefix()) - if (address != null) { - Log.i("[Context] Transferring current call to $addressToCall") - currentCall.transferTo(address) - return true - } - } - return false - } - - fun startCall(to: String) { - var stringAddress = to.trim() - if (android.util.Patterns.PHONE.matcher(to).matches()) { - val contact = contactsManager.findContactByPhoneNumber(to) - val alias = contact?.getContactForPhoneNumberOrAddress(to) - if (alias != null) { - Log.i("[Context] Found matching alias $alias for phone number $to, using it") - stringAddress = alias - } - } - - val address: Address? = core.interpretUrl( - stringAddress, - LinphoneUtils.applyInternationalPrefix() - ) - if (address == null) { - Log.e("[Context] Failed to parse $stringAddress, abort outgoing call") - callErrorMessageResourceId.value = Event( - context.getString(R.string.call_error_network_unreachable) - ) - return - } - - startCall(address) - } - - fun startCall( - address: Address, - callParams: CallParams? = null, - forceZRTP: Boolean = false, - localAddress: Address? = null - ) { - if (!core.isNetworkReachable) { - Log.e("[Context] Network unreachable, abort outgoing call") - callErrorMessageResourceId.value = Event( - context.getString(R.string.call_error_network_unreachable) - ) - return - } - - val params = callParams ?: core.createCallParams(null) - if (params == null) { - val call = core.inviteAddress(address) - Log.w("[Context] Starting call $call without params") - return - } - - if (forceZRTP) { - params.mediaEncryption = MediaEncryption.ZRTP - } - if (LinphoneUtils.checkIfNetworkHasLowBandwidth(context)) { - Log.w("[Context] Enabling low bandwidth mode!") - params.isLowBandwidthEnabled = true - } - params.recordFile = LinphoneUtils.getRecordingFilePathForAddress(address) - - if (localAddress != null) { - val account = core.accountList.find { account -> - account.params.identityAddress?.weakEqual(localAddress) ?: false - } - if (account != null) { - params.account = account - Log.i( - "[Context] Using account matching address ${localAddress.asStringUriOnly()} as From" - ) - } else { - Log.e( - "[Context] Failed to find account matching address ${localAddress.asStringUriOnly()}" - ) - } - } - - if (corePreferences.sendEarlyMedia) { - params.isEarlyMediaSendingEnabled = true - } - - val call = core.inviteAddressWithParams(address, params) - Log.i("[Context] Starting call $call") - } - - fun switchCamera() { - val currentDevice = core.videoDevice - Log.i("[Context] Current camera device is $currentDevice") - - for (camera in core.videoDevicesList) { - if (camera != currentDevice && camera != "StaticImage: Static picture") { - Log.i("[Context] New camera device will be $camera") - core.videoDevice = camera - break - } - } - - val conference = core.conference - if (conference == null || !conference.isIn) { - val call = core.currentCall - if (call == null) { - Log.w("[Context] Switching camera while not in call") - return - } - call.update(null) - } - } - - fun showSwitchCameraButton(): Boolean { - return !corePreferences.disableVideo && core.videoDevicesList.size > 2 // Count StaticImage camera - } - - fun createCallOverlay() { - if (!corePreferences.showCallOverlay || !corePreferences.systemWideCallOverlay || callOverlay != null) { - return - } - - if (overlayY == 0f) overlayY = AppUtils.pixelsToDp(40f) - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - // WRAP_CONTENT doesn't work well on some launchers... - val params: WindowManager.LayoutParams = WindowManager.LayoutParams( - AppUtils.getDimension(R.dimen.call_overlay_size).toInt(), - AppUtils.getDimension(R.dimen.call_overlay_size).toInt(), - Compatibility.getOverlayType(), - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSLUCENT - ) - params.x = overlayX.toInt() - params.y = overlayY.toInt() - params.gravity = Gravity.TOP or Gravity.CENTER_HORIZONTAL - val overlay = LayoutInflater.from(context).inflate(R.layout.call_overlay, null) - - var initX = overlayX - var initY = overlayY - overlay.setOnTouchListener { view, event -> - when (event.action) { - MotionEvent.ACTION_DOWN -> { - initX = params.x - event.rawX - initY = params.y - event.rawY - } - MotionEvent.ACTION_MOVE -> { - val x = (event.rawX + initX).toInt() - val y = (event.rawY + initY).toInt() - - params.x = x - params.y = y - windowManager.updateViewLayout(overlay, params) - } - MotionEvent.ACTION_UP -> { - if (abs(overlayX - params.x) < CorePreferences.OVERLAY_CLICK_SENSITIVITY && - abs(overlayY - params.y) < CorePreferences.OVERLAY_CLICK_SENSITIVITY - ) { - view.performClick() - } - overlayX = params.x.toFloat() - overlayY = params.y.toFloat() - } - else -> return@setOnTouchListener false - } - true - } - overlay.setOnClickListener { - onCallOverlayClick() - } - - try { - windowManager.addView(overlay, params) - callOverlay = overlay - } catch (e: Exception) { - Log.e("[Context] Failed to add overlay in windowManager: $e") - } - } - - fun onCallOverlayClick() { - val call = core.currentCall ?: core.calls.firstOrNull() - if (call != null) { - Log.i("[Context] Overlay clicked, go back to call view") - when (call.state) { - Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> onIncomingReceived() - Call.State.OutgoingInit, Call.State.OutgoingProgress, Call.State.OutgoingRinging, Call.State.OutgoingEarlyMedia -> onOutgoingStarted() - else -> onCallStarted() - } - } else { - Log.e("[Context] Couldn't find call, why is the overlay clicked?!") - } - } - - fun removeCallOverlay() { - if (callOverlay != null) { - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - windowManager.removeView(callOverlay) - callOverlay = null - } - } - - /* Coroutine related */ - - private fun exportFileInMessage(message: ChatMessage) { - // Only do it if auto download feature isn't disabled, otherwise it's done in the user-initiated download process - if (core.maxSizeForAutoDownloadIncomingFiles != -1) { - var hasFile = false - for (content in message.contents) { - if (content.isFile) { - hasFile = true - break - } - } - if (hasFile) { - exportFilesInMessageToMediaStore(message) - } - } - } - - private fun exportFilesInMessageToMediaStore(message: ChatMessage) { - if (message.isEphemeral) { - Log.w("[Context] Do not make ephemeral file(s) public") - return - } - if (corePreferences.vfsEnabled) { - Log.w("[Context] [VFS] Do not make received file(s) public when VFS is enabled") - return - } - if (!corePreferences.makePublicMediaFilesDownloaded) { - Log.w("[Context] Making received files public setting disabled") - return - } - - if (PermissionHelper.get().hasWriteExternalStoragePermission()) { - for (content in message.contents) { - if (content.isFile && content.filePath != null && content.userData == null) { - Log.i("[Context] Trying to export file [${content.name}] to MediaStore") - addContentToMediaStore(content) - } - } - } else { - Log.e( - "[Context] Can't make file public, app doesn't have WRITE_EXTERNAL_STORAGE permission" - ) - } - } - - fun addContentToMediaStore(content: Content) { - if (corePreferences.vfsEnabled) { - Log.w("[Context] [VFS] Do not make received file(s) public when VFS is enabled") - return - } - if (!corePreferences.makePublicMediaFilesDownloaded) { - Log.w("[Context] Making received files public setting disabled") - return - } - - if (PermissionHelper.get().hasWriteExternalStoragePermission()) { - coroutineScope.launch { - val filePath = content.filePath.orEmpty() - Log.i("[Context] Trying to export file [$filePath] through Media Store API") - - val extension = FileUtils.getExtensionFromFileName(filePath) - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - when (FileUtils.getMimeType(mime)) { - FileUtils.MimeType.Image -> { - if (Compatibility.addImageToMediaStore(context, content)) { - Log.i( - "[Context] Successfully exported image [${content.name}] to Media Store" - ) - } else { - Log.e( - "[Context] Something went wrong while copying file to Media Store..." - ) - } - } - FileUtils.MimeType.Video -> { - if (Compatibility.addVideoToMediaStore(context, content)) { - Log.i( - "[Context] Successfully exported video [${content.name}] to Media Store" - ) - } else { - Log.e( - "[Context] Something went wrong while copying file to Media Store..." - ) - } - } - FileUtils.MimeType.Audio -> { - if (Compatibility.addAudioToMediaStore(context, content)) { - Log.i( - "[Context] Successfully exported audio [${content.name}] to Media Store" - ) - } else { - Log.e( - "[Context] Something went wrong while copying file to Media Store..." - ) - } - } - else -> { - Log.w( - "[Context] File [$filePath] isn't either an image, an audio file or a video [${content.type}/${content.subtype}], can't add it to the Media Store" - ) - } - } - } - } - } - - fun checkIfForegroundServiceNotificationCanBeRemovedAfterDelay(delayInMs: Long) { - coroutineScope.launch { - withContext(Dispatchers.Default) { - delay(delayInMs) - withContext(Dispatchers.Main) { - if (core.defaultAccount != null && core.defaultAccount?.state == RegistrationState.Ok) { - Log.i( - "[Context] Default account is registered, cancel foreground service notification if possible" - ) - notificationsManager.stopForegroundNotificationIfPossible() - } - } - } - } - } - - /* Start call related activities */ - - private fun onIncomingReceived() { - if (corePreferences.preventInterfaceFromShowingUp) { - Log.w("[Context] We were asked to not show the incoming call screen") - return - } - - Log.i("[Context] Starting IncomingCallActivity") - val intent = Intent(context, org.linphone.activities.voip.CallActivity::class.java) - // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - context.startActivity(intent) - } - - private fun onOutgoingStarted() { - if (corePreferences.preventInterfaceFromShowingUp) { - Log.w("[Context] We were asked to not show the outgoing call screen") - return - } - - Log.i("[Context] Starting OutgoingCallActivity") - val intent = Intent(context, org.linphone.activities.voip.CallActivity::class.java) - // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - context.startActivity(intent) - } - - fun onCallStarted() { - if (corePreferences.preventInterfaceFromShowingUp) { - Log.w("[Context] We were asked to not show the call screen") - return - } - - Log.i("[Context] Starting CallActivity") - val intent = Intent(context, org.linphone.activities.voip.CallActivity::class.java) - // This flag is required to start an Activity from a Service context - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - context.startActivity(intent) - } - - /* VFS */ - - companion object { - private const val TRANSFORMATION = "AES/GCM/NoPadding" - private const val ANDROID_KEY_STORE = "AndroidKeyStore" - private const val ALIAS = "vfs" - private const val LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256 = 2 - private const val VFS_IV = "vfsiv" - private const val VFS_KEY = "vfskey" - - @Throws(java.lang.Exception::class) - private fun generateSecretKey() { - val keyGenerator = - KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE) - keyGenerator.init( - KeyGenParameterSpec.Builder( - ALIAS, - KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT - ) - .setBlockModes(KeyProperties.BLOCK_MODE_GCM) - .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) - .build() - ) - keyGenerator.generateKey() - } - - @Throws(java.lang.Exception::class) - private fun getSecretKey(): SecretKey? { - val ks = KeyStore.getInstance(ANDROID_KEY_STORE) - ks.load(null) - val entry = ks.getEntry(ALIAS, null) as KeyStore.SecretKeyEntry - return entry.secretKey - } - - @Throws(java.lang.Exception::class) - fun generateToken(): String { - return sha512(UUID.randomUUID().toString()) - } - - @Throws(java.lang.Exception::class) - private fun encryptData(textToEncrypt: String): Pair { - val cipher = Cipher.getInstance(TRANSFORMATION) - cipher.init(Cipher.ENCRYPT_MODE, getSecretKey()) - val iv = cipher.iv - return Pair( - iv, - cipher.doFinal(textToEncrypt.toByteArray(StandardCharsets.UTF_8)) - ) - } - - @Throws(java.lang.Exception::class) - private fun decryptData(encrypted: String?, encryptionIv: ByteArray): String { - val cipher = Cipher.getInstance(TRANSFORMATION) - val spec = GCMParameterSpec(128, encryptionIv) - cipher.init(Cipher.DECRYPT_MODE, getSecretKey(), spec) - val encryptedData = Base64.decode(encrypted, Base64.DEFAULT) - return String(cipher.doFinal(encryptedData), StandardCharsets.UTF_8) - } - - @Throws(java.lang.Exception::class) - fun encryptToken(string_to_encrypt: String): Pair { - val encryptedData = encryptData(string_to_encrypt) - return Pair( - Base64.encodeToString(encryptedData.first, Base64.DEFAULT), - Base64.encodeToString(encryptedData.second, Base64.DEFAULT) - ) - } - - @Throws(java.lang.Exception::class) - fun sha512(input: String): String { - val md = MessageDigest.getInstance("SHA-512") - val messageDigest = md.digest(input.toByteArray()) - val no = BigInteger(1, messageDigest) - var hashtext = no.toString(16) - while (hashtext.length < 32) { - hashtext = "0$hashtext" - } - return hashtext - } - - @Throws(java.lang.Exception::class) - fun getVfsKey(sharedPreferences: SharedPreferences): String { - val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE) - keyStore.load(null) - return decryptData( - sharedPreferences.getString(VFS_KEY, null), - Base64.decode(sharedPreferences.getString(VFS_IV, null), Base64.DEFAULT) - ) - } - - fun activateVFS() { - try { - Log.i("[Context] [VFS] Activating VFS") - val preferences = corePreferences.encryptedSharedPreferences - if (preferences == null) { - Log.e("[Context] [VFS] Can't get encrypted SharedPreferences, can't init VFS") - return - } - - if (preferences.getString(VFS_IV, null) == null) { - generateSecretKey() - encryptToken(generateToken()).let { data -> - preferences - .edit() - .putString(VFS_IV, data.first) - .putString(VFS_KEY, data.second) - .commit() - } - } - Factory.instance().setVfsEncryption( - LINPHONE_VFS_ENCRYPTION_AES256GCM128_SHA256, - getVfsKey(preferences).toByteArray().copyOfRange(0, 32), - 32 - ) - - Log.i("[Context] [VFS] VFS activated") - } catch (e: Exception) { - Log.f("[Context] [VFS] Unable to activate VFS encryption: $e") - } - } - } -} diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt deleted file mode 100644 index e65c90932..000000000 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ /dev/null @@ -1,769 +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 . - */ -package org.linphone.core - -import android.content.Context -import android.content.SharedPreferences -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.security.KeyStoreException -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.compatibility.Compatibility -import org.linphone.core.tools.Log -import org.linphone.utils.LinphoneUtils - -class CorePreferences constructor(private val context: Context) { - private var _config: Config? = null - var config: Config - get() = _config ?: coreContext.core.config - set(value) { - _config = value - } - - /* VFS encryption */ - - companion object { - const val OVERLAY_CLICK_SENSITIVITY = 10 - - private const val encryptedSharedPreferencesFile = "encrypted.pref" - } - - val encryptedSharedPreferences: SharedPreferences? by lazy { - val masterKey: MasterKey = MasterKey.Builder( - context, - MasterKey.DEFAULT_MASTER_KEY_ALIAS - ).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build() - - try { - EncryptedSharedPreferences.create( - context, - encryptedSharedPreferencesFile, - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - } catch (kse: KeyStoreException) { - Log.e("[VFS] Keystore exception: $kse") - null - } catch (e: Exception) { - Log.e("[VFS] Exception: $e") - null - } - } - - var vfsEnabled: Boolean - get() = encryptedSharedPreferences?.getBoolean("vfs_enabled", false) ?: false - set(value) { - val preferences = encryptedSharedPreferences - if (preferences == null) { - Log.e("[VFS] Failed to get encrypted SharedPreferences") - return - } - - if (!value && preferences.getBoolean("vfs_enabled", false)) { - Log.w("[VFS] It is not possible to disable VFS once it has been enabled") - return - } - - preferences.edit().putBoolean("vfs_enabled", value)?.apply() - // When VFS is enabled we disable logcat output for linphone logs - // TODO: decide if we do it - // logcatLogsOutput = false - } - - /* App settings */ - - var debugLogs: Boolean - get() = config.getBool("app", "debug", org.linphone.BuildConfig.DEBUG) - set(value) { - config.setBool("app", "debug", value) - } - - var logcatLogsOutput: Boolean - get() = config.getBool("app", "print_logs_into_logcat", true) - set(value) { - config.setBool("app", "print_logs_into_logcat", value) - } - - var autoStart: Boolean - get() = config.getBool("app", "auto_start", true) - set(value) { - config.setBool("app", "auto_start", value) - } - - var keepServiceAlive: Boolean - get() = config.getBool("app", "keep_service_alive", false) - set(value) { - config.setBool("app", "keep_service_alive", value) - } - - var readAndAgreeTermsAndPrivacy: Boolean - get() = config.getBool("app", "read_and_agree_terms_and_privacy", false) - set(value) { - config.setBool("app", "read_and_agree_terms_and_privacy", value) - } - - /* UI */ - - var forcePortrait: Boolean - get() = config.getBool("app", "force_portrait_orientation", false) - set(value) { - config.setBool("app", "force_portrait_orientation", value) - } - - var replaceSipUriByUsername: Boolean - get() = config.getBool("app", "replace_sip_uri_by_username", false) - set(value) { - config.setBool("app", "replace_sip_uri_by_username", value) - } - - var enableAnimations: Boolean - get() = config.getBool("app", "enable_animations", false) - set(value) { - config.setBool("app", "enable_animations", value) - } - - /** -1 means auto, 0 no, 1 yes */ - var darkMode: Int - get() { - if (!darkModeAllowed) return 0 - return config.getInt("app", "dark_mode", -1) - } - set(value) { - config.setInt("app", "dark_mode", value) - } - - /** Allow to make screenshots of encrypted chat rooms, disables fragment's secure mode */ - var disableSecureMode: Boolean - get() = config.getBool("app", "disable_fragment_secure_mode", false) - set(value) { - config.setBool("app", "disable_fragment_secure_mode", value) - } - - /* Audio */ - - /* Video */ - - var videoPreview: Boolean - get() = config.getBool("app", "video_preview", false) - set(value) = config.setBool("app", "video_preview", value) - - /* Chat */ - - var preventMoreThanOneFilePerMessage: Boolean - get() = config.getBool("app", "prevent_more_than_one_file_per_message", false) - set(value) { - config.setBool("app", "prevent_more_than_one_file_per_message", value) - } - - var markAsReadUponChatMessageNotificationDismissal: Boolean - get() = config.getBool("app", "mark_as_read_notif_dismissal", false) - set(value) { - config.setBool("app", "mark_as_read_notif_dismissal", value) - } - - var makePublicMediaFilesDownloaded: Boolean - // Keep old name for backward compatibility - get() = config.getBool("app", "make_downloaded_images_public_in_gallery", true) - set(value) { - config.setBool("app", "make_downloaded_images_public_in_gallery", value) - } - - var useInAppFileViewerForNonEncryptedFiles: Boolean - get() = config.getBool("app", "use_in_app_file_viewer_for_non_encrypted_files", false) - set(value) { - config.setBool("app", "use_in_app_file_viewer_for_non_encrypted_files", value) - } - - var hideChatMessageContentInNotification: Boolean - get() = config.getBool("app", "hide_chat_message_content_in_notification", false) - set(value) { - config.setBool("app", "hide_chat_message_content_in_notification", value) - } - - var hideEmptyRooms: Boolean - get() = config.getBool("misc", "hide_empty_chat_rooms", true) - set(value) { - config.setBool("misc", "hide_empty_chat_rooms", value) - } - - var hideRoomsFromRemovedProxies: Boolean - get() = config.getBool("misc", "hide_chat_rooms_from_removed_proxies", true) - set(value) { - config.setBool("misc", "hide_chat_rooms_from_removed_proxies", value) - } - - var deviceName: String - get() = config.getString("app", "device_name", Compatibility.getDeviceName(context))!! - set(value) = config.setString("app", "device_name", value) - - var chatRoomShortcuts: Boolean - get() = config.getBool("app", "chat_room_shortcuts", true) - set(value) { - config.setBool("app", "chat_room_shortcuts", value) - } - - /* Voice Recordings */ - - var voiceRecordingMaxDuration: Int - get() = config.getInt("app", "voice_recording_max_duration", 600000) // in ms - set(value) = config.setInt("app", "voice_recording_max_duration", value) - - var holdToRecordVoiceMessage: Boolean - get() = config.getBool("app", "voice_recording_hold_and_release_mode", false) - set(value) = config.setBool("app", "voice_recording_hold_and_release_mode", value) - - var sendVoiceRecordingRightAway: Boolean - get() = config.getBool("app", "voice_recording_send_right_away", false) - set(value) = config.setBool("app", "voice_recording_send_right_away", value) - - /* Contacts */ - - var storePresenceInNativeContact: Boolean - get() = config.getBool("app", "store_presence_in_native_contact", false) - set(value) { - config.setBool("app", "store_presence_in_native_contact", value) - } - - var showNewContactAccountDialog: Boolean - get() = config.getBool("app", "show_new_contact_account_dialog", true) - set(value) { - config.setBool("app", "show_new_contact_account_dialog", value) - } - - var displayOrganization: Boolean - get() = config.getBool("app", "display_contact_organization", contactOrganizationVisible) - set(value) { - config.setBool("app", "display_contact_organization", value) - } - - var contactsShortcuts: Boolean - get() = config.getBool("app", "contact_shortcuts", false) - set(value) { - config.setBool("app", "contact_shortcuts", value) - } - - var publishPresence: Boolean - get() = config.getBool("app", "publish_presence", true) - set(value) { - config.setBool("app", "publish_presence", value) - } - - /* Call */ - - var sendEarlyMedia: Boolean - get() = config.getBool("sip", "outgoing_calls_early_media", false) - set(value) { - config.setBool("sip", "outgoing_calls_early_media", value) - } - - var acceptEarlyMedia: Boolean - get() = config.getBool("sip", "incoming_calls_early_media", false) - set(value) { - config.setBool("sip", "incoming_calls_early_media", value) - } - - var autoAnswerEnabled: Boolean - get() = config.getBool("app", "auto_answer", false) - set(value) { - config.setBool("app", "auto_answer", value) - } - - var autoAnswerDelay: Int - get() = config.getInt("app", "auto_answer_delay", 0) - set(value) { - config.setInt("app", "auto_answer_delay", value) - } - - // Show overlay inside of application - var showCallOverlay: Boolean - get() = config.getBool("app", "call_overlay", true) - set(value) { - config.setBool("app", "call_overlay", value) - } - - // Show overlay even when app is in background, requires permission - var systemWideCallOverlay: Boolean - get() = config.getBool("app", "system_wide_call_overlay", false) - set(value) { - config.setBool("app", "system_wide_call_overlay", value) - } - - var callRightAway: Boolean - get() = config.getBool("app", "call_right_away", false) - set(value) { - config.setBool("app", "call_right_away", value) - } - - // Will send user to contacts list directly - var skipDialerForNewCallAndTransfer: Boolean - get() = config.getBool("app", "skip_dialer_for_new_call_and_transfer", false) - set(value) { - config.setBool("app", "skip_dialer_for_new_call_and_transfer", value) - } - - var automaticallyStartCallRecording: Boolean - get() = config.getBool("app", "auto_start_call_record", false) - set(value) { - config.setBool("app", "auto_start_call_record", value) - } - - var useTelecomManager: Boolean - // Some permissions are required, so keep it to false so user has to manually enable it and give permissions - get() = config.getBool("app", "use_self_managed_telecom_manager", false) - set(value) { - config.setBool("app", "use_self_managed_telecom_manager", value) - // We need to disable audio focus requests when enabling telecom manager, otherwise it creates conflicts - config.setBool("audio", "android_disable_audio_focus_requests", value) - } - - // We will try to auto enable Telecom Manager feature, but in case user disables it don't try again - var manuallyDisabledTelecomManager: Boolean - get() = config.getBool("app", "user_disabled_self_managed_telecom_manager", false) - set(value) { - config.setBool("app", "user_disabled_self_managed_telecom_manager", value) - } - - // Also uses Hearing Aids if available - var routeAudioToBluetoothIfAvailable: Boolean - get() = config.getBool("app", "route_audio_to_bluetooth_if_available", true) - set(value) { - config.setBool("app", "route_audio_to_bluetooth_if_available", value) - } - - // This won't be done if bluetooth or wired headset is used - var routeAudioToSpeakerWhenVideoIsEnabled: Boolean - get() = config.getBool("app", "route_audio_to_speaker_when_video_enabled", true) - set(value) { - config.setBool("app", "route_audio_to_speaker_when_video_enabled", value) - } - - // Automatically handled by SDK - var pauseCallsWhenAudioFocusIsLost: Boolean - get() = config.getBool("audio", "android_pause_calls_when_audio_focus_lost", true) - set(value) { - config.setBool("audio", "android_pause_calls_when_audio_focus_lost", value) - } - - var enableFullScreenWhenJoiningVideoCall: Boolean - get() = config.getBool("app", "enter_video_call_enable_full_screen_mode", false) - set(value) { - config.setBool("app", "enter_video_call_enable_full_screen_mode", value) - } - - var enableFullScreenWhenJoiningVideoConference: Boolean - get() = config.getBool("app", "enter_video_conference_enable_full_screen_mode", true) - set(value) { - config.setBool("app", "enter_video_conference_enable_full_screen_mode", value) - } - - var disableBroadcastConference: Boolean - get() = config.getBool("app", "disable_broadcast_conference_feature", true) - set(value) { - config.setBool("app", "disable_broadcast_conference_feature", value) - } - - /* Assistant */ - - var firstStart: Boolean - get() = config.getBool("app", "first_start", true) - set(value) { - config.setBool("app", "first_start", value) - } - - var xmlRpcServerUrl: String? - get() = config.getString("assistant", "xmlrpc_url", null) - set(value) { - config.setString("assistant", "xmlrpc_url", value) - } - - var hideLinkPhoneNumber: Boolean - get() = config.getBool("app", "hide_link_phone_number", false) - set(value) { - config.setBool("app", "hide_link_phone_number", value) - } - - /* Dialog related */ - - var limeSecurityPopupEnabled: Boolean - get() = config.getBool("app", "lime_security_popup_enabled", true) - set(value) { - config.setBool("app", "lime_security_popup_enabled", value) - } - - /* Other */ - - var voiceMailUri: String? - get() = config.getString("app", "voice_mail", null) - set(value) { - config.setString("app", "voice_mail", value) - } - - var redirectDeclinedCallToVoiceMail: Boolean - get() = config.getBool("app", "redirect_declined_call_to_voice_mail", true) - set(value) { - config.setBool("app", "redirect_declined_call_to_voice_mail", value) - } - - var lastUpdateAvailableCheckTimestamp: Int - get() = config.getInt("app", "version_check_url_last_timestamp", 0) - set(value) { - config.setInt("app", "version_check_url_last_timestamp", value) - } - - var defaultAccountAvatarPath: String? - get() = config.getString("app", "default_avatar_path", null) - set(value) { - config.setString("app", "default_avatar_path", value) - } - - /* *** Read only application settings, some were previously in non_localizable_custom *** */ - - /* UI related */ - - val contactOrganizationVisible: Boolean - get() = config.getBool("app", "display_contact_organization", true) - - private val darkModeAllowed: Boolean - get() = config.getBool("app", "dark_mode_allowed", true) - - /* Feature related */ - - val showScreenshotButton: Boolean - get() = config.getBool("app", "show_take_screenshot_button_in_call", false) - - val dtmfKeypadVibration: Boolean - get() = config.getBool("app", "dtmf_keypad_vibraton", false) - - val allowMultipleFilesAndTextInSameMessage: Boolean - get() = config.getBool("app", "allow_multiple_files_and_text_in_same_message", true) - - val enableNativeAddressBookIntegration: Boolean - get() = config.getBool("app", "enable_native_address_book", true) - - val fetchContactsFromDefaultDirectory: Boolean - get() = config.getBool("app", "fetch_contacts_from_default_directory", true) - - val delayBeforeShowingContactsSearchSpinner: Int - get() = config.getInt("app", "delay_before_showing_contacts_search_spinner", 200) - - // From Android Contact APIs we can also retrieve the internationalized phone number - // By default we display the same value as the native address book app - val preferNormalizedPhoneNumbersFromAddressBook: Boolean - get() = config.getBool("app", "prefer_normalized_phone_numbers_from_address_book", false) - - val hideStaticImageCamera: Boolean - get() = config.getBool("app", "hide_static_image_camera", true) - - // Will prevent user adding contact and editing / removing existing contacts - val readOnlyNativeContacts: Boolean - get() = config.getBool("app", "read_only_native_address_book", false) - - // Will hide the contacts selector to allow listing all contacts, even those without a SIP address - val onlyShowSipContactsList: Boolean - get() = config.getBool("app", "only_show_sip_contacts_list", false) - - // Will hide the SIP contacts selector, leaving only the all contacts list - val hideSipContactsList: Boolean - get() = config.getBool("app", "hide_sip_contacts_list", false) - - // Will disable chat feature completely - val disableChat: Boolean - get() = config.getBool("app", "disable_chat_feature", false) - - // Will disable video feature completely - val disableVideo: Boolean - get() = config.getBool("app", "disable_video_feature", false) - - val forceEndToEndEncryptedChat: Boolean - get() = config.getBool("app", "force_lime_chat_rooms", false) - - // Turning this ON will show the secure chat button even if there is no LIME capability in presence (or no presence) - val allowEndToEndEncryptedChatWithoutPresence: Boolean - get() = config.getBool("app", "allow_lime_friend_without_capability", false) - - val showEmojiPickerButton: Boolean - get() = config.getBool("app", "show_emoji_picker", true) - - // This will prevent UI from showing up, except for the launcher & the foreground service notification - val preventInterfaceFromShowingUp: Boolean - get() = config.getBool("app", "keep_app_invisible", false) - - // By default we will record voice messages using MKV format and Opus audio encoding - // If disabled, WAV format will be used instead. Warning: files will be heavier! - val voiceMessagesFormatMkv: Boolean - get() = config.getBool("app", "record_voice_messages_in_mkv_format", true) - - val useEphemeralPerDeviceMode: Boolean - get() = config.getBool("app", "ephemeral_chat_messages_settings_per_device", true) - - // If enabled user will see all ringtones bundled in our SDK - // and will be able to choose which one to use if not using it's device's default - val showAllRingtones: Boolean - get() = config.getBool("app", "show_all_available_ringtones", false) - - val showContactInviteBySms: Boolean - get() = config.getBool("app", "show_invite_contact_by_sms", true) - - val autoRemoteProvisioningOnConfigUriHandler: Boolean - get() = config.getBool("app", "auto_apply_provisioning_config_uri_handler", false) - - val askForAccountPasswordToAccessSettings: Boolean - get() = config.getBool("app", "require_password_to_access_settings", false) - - /* Default values related */ - - val defaultDomain: String - get() = config.getString("app", "default_domain", "sip.linphone.org")!! - - val defaultRlsUri: String - get() = config.getString("sip", "rls_uri", "sips:rls@sip.linphone.org")!! - - val debugPopupCode: String - get() = config.getString("app", "debug_popup_magic", "#1234#")!! - - // If there is more participants than this value in a conference, force ActiveSpeaker layout - val maxConferenceParticipantsForMosaicLayout: Int - get() = config.getInt("app", "conference_mosaic_layout_max_participants", 6) - - val conferenceServerUri: String - get() = config.getString( - "app", - "default_conference_factory_uri", - "sip:conference-factory@sip.linphone.org" - )!! - - val audioVideoConferenceServerUri: String - get() = config.getString( - "app", - "default_audio_video_conference_factory_uri", - "sip:videoconference-factory@sip.linphone.org" - )!! - - val limeServerUrl: String - get() = config.getString( - "app", - "default_lime_server_url", - "https://lime.linphone.org/lime-server/lime-server.php" - )!! - - val checkUpdateAvailableInterval: Int - get() = config.getInt("app", "version_check_interval", 86400000) - - /* Assistant */ - - val showCreateAccount: Boolean - get() = config.getBool("app", "assistant_create_account", true) - - val showLinphoneLogin: Boolean - get() = config.getBool("app", "assistant_linphone_login", true) - - val showGenericLogin: Boolean - get() = config.getBool("app", "assistant_generic_login", true) - - val showRemoteProvisioning: Boolean - get() = config.getBool("app", "assistant_remote_provisioning", true) - - /* Side Menu */ - - val showAccountsInSideMenu: Boolean - get() = config.getBool("app", "side_menu_accounts", true) - - val showAssistantInSideMenu: Boolean - get() = config.getBool("app", "side_menu_assistant", true) - - val showSettingsInSideMenu: Boolean - get() = config.getBool("app", "side_menu_settings", true) - - val showRecordingsInSideMenu: Boolean - get() = config.getBool("app", "side_menu_recordings", true) - - val showScheduledConferencesInSideMenu: Boolean - get() = config.getBool( - "app", - "side_menu_conferences", - LinphoneUtils.isRemoteConferencingAvailable() - ) - - val showAboutInSideMenu: Boolean - get() = config.getBool("app", "side_menu_about", true) - - val showQuitInSideMenu: Boolean - get() = config.getBool("app", "side_menu_quit", true) - - /* Settings */ - - val allowDtlsTransport: Boolean - get() = config.getBool("app", "allow_dtls_transport", false) - - val showAccountSettings: Boolean - get() = config.getBool("app", "settings_accounts", true) - - val showTunnelSettings: Boolean - get() = config.getBool("app", "settings_tunnel", true) - - val showAudioSettings: Boolean - get() = config.getBool("app", "settings_audio", true) - - val showVideoSettings: Boolean - get() = config.getBool("app", "settings_video", !disableVideo) - - val showCallSettings: Boolean - get() = config.getBool("app", "settings_call", true) - - val showChatSettings: Boolean - get() = config.getBool("app", "settings_chat", !disableChat) - - val showNetworkSettings: Boolean - get() = config.getBool("app", "settings_network", true) - - val showContactsSettings: Boolean - get() = config.getBool("app", "settings_contacts", true) - - val showAdvancedSettings: Boolean - get() = config.getBool("app", "settings_advanced", true) - - val showConferencesSettings: Boolean - get() = config.getBool( - "app", - "settings_conferences", - LinphoneUtils.isRemoteConferencingAvailable() - ) - - /* Assets stuff */ - - val configPath: String - get() = context.filesDir.absolutePath + "/.linphonerc" - - val factoryConfigPath: String - get() = context.filesDir.absolutePath + "/linphonerc" - - val linphoneDefaultValuesPath: String - get() = context.filesDir.absolutePath + "/assistant_linphone_default_values" - - val defaultValuesPath: String - get() = context.filesDir.absolutePath + "/assistant_default_values" - - val ringtonesPath: String - get() = context.filesDir.absolutePath + "/share/sounds/linphone/rings/" - - val defaultRingtonePath: String - get() = ringtonesPath + "notes_of_the_optimistic.mkv" - - val userCertificatesPath: String - get() = context.filesDir.absolutePath + "/user-certs" - - val staticPicturePath: String - get() = context.filesDir.absolutePath + "/share/images/nowebcamcif.jpg" - - val vfsCachePath: String - get() = context.cacheDir.absolutePath + "/evfs/" - - fun copyAssetsFromPackage() { - copy("linphonerc_default", configPath) - copy("linphonerc_factory", factoryConfigPath, true) - copy("assistant_linphone_default_values", linphoneDefaultValuesPath, true) - copy("assistant_default_values", defaultValuesPath, true) - - move( - context.filesDir.absolutePath + "/linphone-log-history.db", - context.filesDir.absolutePath + "/call-history.db" - ) - move( - context.filesDir.absolutePath + "/zrtp_secrets", - context.filesDir.absolutePath + "/zrtp-secrets.db" - ) - } - - fun getString(resource: Int): String { - return context.getString(resource) - } - - private fun copy(from: String, to: String, overrideIfExists: Boolean = false) { - val outFile = File(to) - if (outFile.exists()) { - if (!overrideIfExists) { - android.util.Log.i( - context.getString(org.linphone.R.string.app_name), - "[Preferences] File $to already exists" - ) - return - } - } - android.util.Log.i( - context.getString(org.linphone.R.string.app_name), - "[Preferences] Overriding $to by $from asset" - ) - - val outStream = FileOutputStream(outFile) - val inFile = context.assets.open(from) - val buffer = ByteArray(1024) - var length: Int = inFile.read(buffer) - - while (length > 0) { - outStream.write(buffer, 0, length) - length = inFile.read(buffer) - } - - inFile.close() - outStream.flush() - outStream.close() - } - - private fun move(from: String, to: String, overrideIfExists: Boolean = false) { - val inFile = File(from) - val outFile = File(to) - if (inFile.exists()) { - if (outFile.exists() && !overrideIfExists) { - android.util.Log.w( - context.getString(org.linphone.R.string.app_name), - "[Preferences] Can't move [$from] to [$to], destination file already exists" - ) - } else { - val inStream = FileInputStream(inFile) - val outStream = FileOutputStream(outFile) - - val buffer = ByteArray(1024) - var read: Int - while (inStream.read(buffer).also { read = it } != -1) { - outStream.write(buffer, 0, read) - } - - inStream.close() - outStream.flush() - outStream.close() - - inFile.delete() - android.util.Log.i( - context.getString(org.linphone.R.string.app_name), - "[Preferences] Successfully moved [$from] to [$to]" - ) - } - } else { - android.util.Log.w( - context.getString(org.linphone.R.string.app_name), - "[Preferences] Can't move [$from] to [$to], source file doesn't exists" - ) - } - } -} diff --git a/app/src/main/java/org/linphone/core/CorePushReceiver.kt b/app/src/main/java/org/linphone/core/CorePushReceiver.kt deleted file mode 100644 index d771183df..000000000 --- a/app/src/main/java/org/linphone/core/CorePushReceiver.kt +++ /dev/null @@ -1,33 +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 . - */ -package org.linphone.core - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import org.linphone.LinphoneApplication.Companion.ensureCoreExists -import org.linphone.core.tools.Log - -class CorePushReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - ensureCoreExists(context.applicationContext, true) - Log.i("[Push Notification] Push notification has been received in broadcast receiver") - } -} diff --git a/app/src/main/java/org/linphone/core/CoreService.kt b/app/src/main/java/org/linphone/core/CoreService.kt deleted file mode 100644 index f2f715efb..000000000 --- a/app/src/main/java/org/linphone/core/CoreService.kt +++ /dev/null @@ -1,126 +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 . - */ -package org.linphone.core - -import android.content.Intent -import org.linphone.LinphoneApplication -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.LinphoneApplication.Companion.ensureCoreExists -import org.linphone.core.tools.Log -import org.linphone.core.tools.service.CoreService - -class CoreService : CoreService() { - override fun onCreate() { - super.onCreate() - Log.i("[Service] Created") - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - Log.i("[Service] Starting, ensuring Core exists") - - if (corePreferences.keepServiceAlive) { - Log.i("[Service] Starting as foreground to keep app alive in background") - val contextCreated = ensureCoreExists( - applicationContext, - pushReceived = false, - service = this, - useAutoStartDescription = false - ) - if (!contextCreated) { - // Only start foreground notification if context already exists, otherwise context will do it itself - coreContext.notificationsManager.startForegroundToKeepAppAlive(this, false) - } - } else if (intent?.extras?.get("StartForeground") == true) { - Log.i("[Service] Starting as foreground due to device boot or app update") - val contextCreated = ensureCoreExists( - applicationContext, - pushReceived = false, - service = this, - useAutoStartDescription = true, - skipCoreStart = true - ) - if (contextCreated) { - coreContext.start() - } else { - // Only start foreground notification if context already exists, otherwise context will do it itself - coreContext.notificationsManager.startForegroundToKeepAppAlive(this, true) - } - coreContext.checkIfForegroundServiceNotificationCanBeRemovedAfterDelay(5000) - } else { - ensureCoreExists( - applicationContext, - pushReceived = false, - service = this, - useAutoStartDescription = false - ) - } - - coreContext.notificationsManager.serviceCreated(this) - - return super.onStartCommand(intent, flags, startId) - } - - override fun createServiceNotificationChannel() { - // Done elsewhere - } - - override fun showForegroundServiceNotification(isVideoCall: Boolean) { - Log.i("[Service] Starting service as foreground") - coreContext.notificationsManager.startCallForeground(this) - } - - override fun hideForegroundServiceNotification() { - Log.i("[Service] Stopping service as foreground") - coreContext.notificationsManager.stopCallForeground() - } - - override fun onTaskRemoved(rootIntent: Intent?) { - if (LinphoneApplication.contextExists()) { - if (coreContext.core.callsNb > 0) { - Log.w( - "[Service] Task removed but there is at least one active call, do not stop the Core!" - ) - } else if (!corePreferences.keepServiceAlive) { - if (coreContext.core.isInBackground) { - Log.i("[Service] Task removed, stopping Core") - coreContext.stop() - } else { - Log.w("[Service] Task removed but Core is not in background, skipping") - } - } else { - Log.i( - "[Service] Task removed but we were asked to keep the service alive, so doing nothing" - ) - } - } - - super.onTaskRemoved(rootIntent) - } - - override fun onDestroy() { - if (LinphoneApplication.contextExists()) { - Log.i("[Service] Stopping") - coreContext.notificationsManager.serviceDestroyed() - } - - super.onDestroy() - } -} diff --git a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt b/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt deleted file mode 100644 index 6665f3f1c..000000000 --- a/app/src/main/java/org/linphone/notifications/NotificationBroadcastReceiver.kt +++ /dev/null @@ -1,150 +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 . - */ -package org.linphone.notifications - -import android.app.NotificationManager -import android.app.RemoteInput -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.ensureCoreExists -import org.linphone.core.Call -import org.linphone.core.Core -import org.linphone.core.tools.Log - -class NotificationBroadcastReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - Log.i("[Notification Broadcast Receiver] Ensuring Core exists") - ensureCoreExists(context.applicationContext, false) - - val notificationId = intent.getIntExtra(NotificationsManager.INTENT_NOTIF_ID, 0) - Log.i( - "[Notification Broadcast Receiver] Got notification broadcast for ID [$notificationId]" - ) - - if (intent.action == NotificationsManager.INTENT_REPLY_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_MARK_AS_READ_ACTION) { - handleChatIntent(context, intent, notificationId) - } else if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION || intent.action == NotificationsManager.INTENT_HANGUP_CALL_NOTIF_ACTION) { - handleCallIntent(intent) - } - } - - private fun handleChatIntent(context: Context, intent: Intent, notificationId: Int) { - val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_ADDRESS) - if (remoteSipAddress == null) { - Log.e( - "[Notification Broadcast Receiver] Remote SIP address is null for notification id $notificationId" - ) - return - } - val core: Core = coreContext.core - - val remoteAddress = core.interpretUrl(remoteSipAddress, false) - if (remoteAddress == null) { - Log.e( - "[Notification Broadcast Receiver] Couldn't interpret remote address $remoteSipAddress" - ) - return - } - - val localIdentity = intent.getStringExtra(NotificationsManager.INTENT_LOCAL_IDENTITY) - if (localIdentity == null) { - Log.e( - "[Notification Broadcast Receiver] Local identity is null for notification id $notificationId" - ) - return - } - val localAddress = core.interpretUrl(localIdentity, false) - if (localAddress == null) { - Log.e( - "[Notification Broadcast Receiver] Couldn't interpret local address $localIdentity" - ) - return - } - - val room = core.searchChatRoom(null, localAddress, remoteAddress, arrayOfNulls(0)) - if (room == null) { - Log.e( - "[Notification Broadcast Receiver] Couldn't find chat room for remote address $remoteSipAddress and local address $localIdentity" - ) - return - } - - if (intent.action == NotificationsManager.INTENT_REPLY_NOTIF_ACTION) { - val reply = getMessageText(intent)?.toString() - if (reply == null) { - Log.e("[Notification Broadcast Receiver] Couldn't get reply text") - return - } - - val msg = room.createMessageFromUtf8(reply) - msg.userData = notificationId - msg.addListener(coreContext.notificationsManager.chatListener) - msg.send() - Log.i("[Notification Broadcast Receiver] Reply sent for notif id $notificationId") - } else { - room.markAsRead() - if (!coreContext.notificationsManager.dismissChatNotification(room)) { - Log.w( - "[Notification Broadcast Receiver] Notifications Manager failed to cancel notification" - ) - val notificationManager = context.getSystemService(NotificationManager::class.java) - notificationManager.cancel(NotificationsManager.CHAT_TAG, notificationId) - } - } - } - - private fun handleCallIntent(intent: Intent) { - val remoteSipAddress = intent.getStringExtra(NotificationsManager.INTENT_REMOTE_ADDRESS) - if (remoteSipAddress == null) { - Log.e("[Notification Broadcast Receiver] Remote SIP address is null for notification") - return - } - - val core: Core = coreContext.core - - val remoteAddress = core.interpretUrl(remoteSipAddress, false) - val call = if (remoteAddress != null) core.getCallByRemoteAddress2(remoteAddress) else null - if (call == null) { - Log.e( - "[Notification Broadcast Receiver] Couldn't find call from remote address $remoteSipAddress" - ) - return - } - - if (intent.action == NotificationsManager.INTENT_ANSWER_CALL_NOTIF_ACTION) { - coreContext.answerCall(call) - } else { - if (call.state == Call.State.IncomingReceived || - call.state == Call.State.IncomingEarlyMedia - ) { - coreContext.declineCall(call) - } else { - coreContext.terminateCall(call) - } - } - } - - private fun getMessageText(intent: Intent): CharSequence? { - val remoteInput = RemoteInput.getResultsFromIntent(intent) - return remoteInput?.getCharSequence(NotificationsManager.KEY_TEXT_REPLY) - } -} diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt deleted file mode 100644 index d28715188..000000000 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ /dev/null @@ -1,1385 +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 . - */ -package org.linphone.notifications - -import android.annotation.SuppressLint -import android.app.Notification -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.graphics.Bitmap -import android.net.Uri -import android.os.Bundle -import android.webkit.MimeTypeMap -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import androidx.core.app.Person -import androidx.core.app.RemoteInput -import androidx.core.content.ContextCompat -import androidx.core.content.LocusIdCompat -import androidx.core.graphics.drawable.IconCompat -import androidx.navigation.NavDeepLinkBuilder -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.chat_bubble.ChatBubbleActivity -import org.linphone.activities.main.MainActivity -import org.linphone.activities.voip.CallActivity -import org.linphone.compatibility.Compatibility -import org.linphone.contact.getPerson -import org.linphone.contact.getThumbnailUri -import org.linphone.core.* -import org.linphone.core.tools.Log -import org.linphone.utils.* - -class Notifiable(val notificationId: Int) { - val messages: ArrayList = arrayListOf() - - var isGroup: Boolean = false - var groupTitle: String? = null - var localIdentity: String? = null - var myself: String? = null - var remoteAddress: String? = null -} - -class NotifiableMessage( - var message: String, - val friend: Friend?, - val sender: String, - val time: Long, - val senderAvatar: Bitmap? = null, - var filePath: Uri? = null, - var fileMime: String? = null, - val isOutgoing: Boolean = false, - val isReaction: Boolean = false, - val reactionToMessageId: String? = null, - val reactionFrom: String? = null -) - -class NotificationsManager(private val context: Context) { - companion object { - const val CHAT_NOTIFICATIONS_GROUP = "CHAT_NOTIF_GROUP" - const val KEY_TEXT_REPLY = "key_text_reply" - const val INTENT_NOTIF_ID = "NOTIFICATION_ID" - const val INTENT_REPLY_NOTIF_ACTION = "org.linphone.REPLY_ACTION" - const val INTENT_HANGUP_CALL_NOTIF_ACTION = "org.linphone.HANGUP_CALL_ACTION" - const val INTENT_ANSWER_CALL_NOTIF_ACTION = "org.linphone.ANSWER_CALL_ACTION" - const val INTENT_MARK_AS_READ_ACTION = "org.linphone.MARK_AS_READ_ACTION" - const val INTENT_LOCAL_IDENTITY = "LOCAL_IDENTITY" - const val INTENT_REMOTE_ADDRESS = "REMOTE_ADDRESS" - - private const val SERVICE_NOTIF_ID = 1 - private const val MISSED_CALLS_NOTIF_ID = 10 - - const val CHAT_TAG = "Chat" - private const val MISSED_CALL_TAG = "Missed call" - } - - private val notificationManager: NotificationManagerCompat by lazy { - NotificationManagerCompat.from(context) - } - private val chatNotificationsMap: HashMap = HashMap() - private val callNotificationsMap: HashMap = HashMap() - private val previousChatNotifications: ArrayList = arrayListOf() - private val chatBubbleNotifications: ArrayList = arrayListOf() - - private var currentForegroundServiceNotificationId: Int = 0 - private var serviceNotification: Notification? = null - - private var service: CoreService? = null - - var currentlyDisplayedChatRoomAddress: String? = null - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State, - message: String - ) { - Log.i("[Notifications Manager] Call state changed [$state]") - - if (corePreferences.preventInterfaceFromShowingUp) { - Log.w("[Notifications Manager] We were asked to not show the call notifications") - return - } - - when (call.state) { - Call.State.IncomingEarlyMedia, Call.State.IncomingReceived -> { - if (service != null) { - Log.i( - "[Notifications Manager] Service isn't null, show incoming call notification" - ) - displayIncomingCallNotification(call, false) - } else { - Log.w("[Notifications Manager] No service found, waiting for it to start") - } - } - Call.State.End, Call.State.Error -> dismissCallNotification(call) - Call.State.Released -> { - if (LinphoneUtils.isCallLogMissed(call.callLog)) { - displayMissedCallNotification(call.remoteAddress) - } - } - Call.State.OutgoingInit, Call.State.OutgoingProgress, Call.State.OutgoingRinging -> { - displayCallNotification(call, false) - } - else -> displayCallNotification(call, true) - } - } - - override fun onMessagesReceived( - core: Core, - room: ChatRoom, - messages: Array - ) { - Log.i("[Notifications Manager] Received ${messages.size} aggregated messages") - if (corePreferences.disableChat) return - - if (corePreferences.preventInterfaceFromShowingUp) { - Log.w("[Notifications Manager] We were asked to not show the chat notifications") - return - } - - if (currentlyDisplayedChatRoomAddress == room.peerAddress.asStringUriOnly()) { - Log.i( - "[Notifications Manager] Chat room is currently displayed, do not notify received message" - ) - // Mark as read is now done in the DetailChatRoomFragment - return - } - - if (room.muted) { - val id = LinphoneUtils.getChatRoomId(room.localAddress, room.peerAddress) - Log.i("[Notifications Manager] Chat room $id has been muted") - return - } - - if (corePreferences.chatRoomShortcuts) { - if (ShortcutsHelper.isShortcutToChatRoomAlreadyCreated(context, room)) { - Log.i("[Notifications Manager] Chat room shortcut already exists") - } else { - Log.i( - "[Notifications Manager] Ensure chat room shortcut exists for bubble notification" - ) - ShortcutsHelper.createShortcutsToChatRooms(context) - } - } - - val notifiable = getNotifiableForRoom(room) - val updated = updateChatNotifiableWithMessages(notifiable, room, messages) - if (!updated) { - Log.w( - "[Notifications Manager] No changes made to notifiable, do not display it again" - ) - return - } - - if (notifiable.messages.isNotEmpty()) { - displayChatNotifiable(room, notifiable) - } else { - Log.w( - "[Notifications Manager] No message to display in received aggregated messages" - ) - } - } - - override fun onReactionRemoved( - core: Core, - chatRoom: ChatRoom, - message: ChatMessage, - address: Address - ) { - Log.i( - "[Notifications Manager] [${address.asStringUriOnly()}] removed it's previously sent reaction for chat message [$message]" - ) - if (corePreferences.disableChat) return - - if (chatRoom.muted) { - val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress) - Log.i("[Notifications Manager] Chat room $id has been muted") - return - } - - val chatRoomPeerAddress = chatRoom.peerAddress.asStringUriOnly() - var notifiable: Notifiable? = chatNotificationsMap[chatRoomPeerAddress] - if (notifiable == null) { - Log.i( - "[Notifications Manager] No notification for chat room [$chatRoomPeerAddress], nothing to do" - ) - return - } - - val from = address.asStringUriOnly() - val found = notifiable.messages.find { - it.isReaction && it.reactionToMessageId == message.messageId && it.reactionFrom == from - } - if (found != null) { - if (notifiable.messages.remove(found)) { - if (notifiable.messages.isNotEmpty()) { - Log.i( - "[Notifications Manager] After removing original reaction notification there is still messages, updating notification" - ) - displayChatNotifiable(chatRoom, notifiable) - } else { - Log.i( - "[Notifications Manager] After removing original reaction notification there is nothing left to display, remove notification" - ) - notificationManager.cancel(CHAT_TAG, notifiable.notificationId) - } - } - } else { - Log.w( - "[Notifications Manager] Original reaction not found in currently displayed notification" - ) - } - } - - override fun onNewMessageReaction( - core: Core, - chatRoom: ChatRoom, - message: ChatMessage, - reaction: ChatMessageReaction - ) { - val address = reaction.fromAddress - val defaultAccountAddress = core.defaultAccount?.params?.identityAddress - // Do not notify our own reactions, it won't be done anyway since the chat room is very likely to be currently displayed - if (defaultAccountAddress != null && defaultAccountAddress.weakEqual(address)) return - - Log.i( - "[Notifications Manager] Reaction received [${reaction.body}] from [${address.asStringUriOnly()}] for chat message [$message]" - ) - if (corePreferences.disableChat) return - - if (corePreferences.preventInterfaceFromShowingUp) { - Log.w("[Notifications Manager] We were asked to not show the chat notifications") - return - } - - if (currentlyDisplayedChatRoomAddress == chatRoom.peerAddress.asStringUriOnly()) { - Log.i( - "[Notifications Manager] Chat room is currently displayed, do not notify received reaction" - ) - // Mark as read is now done in the DetailChatRoomFragment - return - } - - if (chatRoom.muted) { - val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress) - Log.i("[Notifications Manager] Chat room $id has been muted") - return - } - - if (coreContext.contactsManager.isAddressMyself(address)) { - Log.i( - "[Notifications Manager] Reaction has been sent by ourselves, do not notify it" - ) - return - } - - if (corePreferences.chatRoomShortcuts) { - if (ShortcutsHelper.isShortcutToChatRoomAlreadyCreated(context, chatRoom)) { - Log.i("[Notifications Manager] Chat room shortcut already exists") - } else { - Log.i( - "[Notifications Manager] Ensure chat room shortcut exists for bubble notification" - ) - ShortcutsHelper.createShortcutsToChatRooms(context) - } - } - - val newNotifiable = createChatReactionNotifiable( - chatRoom, - reaction.body, - address, - message - ) - if (newNotifiable.messages.isNotEmpty()) { - displayChatNotifiable(chatRoom, newNotifiable) - } else { - Log.e( - "[Notifications Manager] Notifiable is empty but we should have displayed the reaction!" - ) - } - } - - override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) { - val address = chatRoom.peerAddress.asStringUriOnly() - val notifiable = chatNotificationsMap[address] - if (notifiable != null) { - if (chatBubbleNotifications.contains(notifiable.notificationId)) { - Log.i( - "[Notifications Manager] Chat room [$chatRoom] has been marked as read, not removing notification because of a chat bubble" - ) - } else { - Log.i( - "[Notifications Manager] Chat room [$chatRoom] has been marked as read, removing notification if any" - ) - dismissChatNotification(chatRoom) - } - } else { - val notificationId = getNotificationIdForChat(chatRoom) - if (chatBubbleNotifications.contains(notificationId)) { - Log.i( - "[Notifications Manager] Chat room [$chatRoom] has been marked as read but no notifiable found, not removing notification because of a chat bubble" - ) - } else { - Log.i( - "[Notifications Manager] Chat room [$chatRoom] has been marked as read but no notifiable found, removing notification if any" - ) - dismissChatNotification(chatRoom) - } - } - } - - override fun onLastCallEnded(core: Core) { - Log.i( - "[Notifications Manager] Last call ended, make sure foreground service is stopped and notification removed" - ) - stopCallForeground() - } - } - - val chatListener: ChatMessageListener = object : ChatMessageListenerStub() { - override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) { - message.userData ?: return - val id = message.userData as Int - Log.i("[Notifications Manager] Reply message state changed [$state] for id $id") - - if (state != ChatMessage.State.InProgress) { - // No need to be called here twice - message.removeListener(this) - } - - if (state == ChatMessage.State.Delivered || state == ChatMessage.State.Displayed) { - val address = message.chatRoom.peerAddress.asStringUriOnly() - val notifiable = chatNotificationsMap[address] - if (notifiable != null) { - if (notifiable.notificationId != id) { - Log.w( - "[Notifications Manager] ID doesn't match: ${notifiable.notificationId} != $id" - ) - } - displayReplyMessageNotification(message, notifiable) - } else { - Log.e( - "[Notifications Manager] Couldn't find notification for chat room $address" - ) - cancel(id, CHAT_TAG) - } - } else if (state == ChatMessage.State.NotDelivered) { - Log.e("[Notifications Manager] Reply wasn't delivered") - cancel(id, CHAT_TAG) - } - } - } - - init { - Compatibility.createNotificationChannels(context, notificationManager) - - val manager = context.getSystemService(NotificationManager::class.java) as NotificationManager - for (notification in manager.activeNotifications) { - if (notification.tag.isNullOrEmpty()) { // We use null tag for call notifications otherwise it will create duplicates when used with Service.startForeground()... - Log.w( - "[Notifications Manager] Found existing call? notification [${notification.id}], cancelling it" - ) - manager.cancel(notification.id) - } else if (notification.tag == CHAT_TAG) { - Log.i( - "[Notifications Manager] Found existing chat notification [${notification.id}]" - ) - previousChatNotifications.add(notification.id) - } - } - } - - fun onCoreReady() { - coreContext.core.addListener(listener) - } - - fun destroy() { - // Don't use cancelAll to keep message notifications ! - // When a message is received by a push, it will create a CoreService - // but it might be getting killed quite quickly after that - // causing the notification to be missed by the user... - Log.i( - "[Notifications Manager] Getting destroyed, clearing foreground Service & call notifications" - ) - - if (currentForegroundServiceNotificationId > 0 && !corePreferences.keepServiceAlive) { - Log.i("[Notifications Manager] Clearing foreground Service") - stopForegroundNotification() - } - - if (callNotificationsMap.size > 0) { - Log.i("[Notifications Manager] Clearing call notifications") - for (notifiable in callNotificationsMap.values) { - notificationManager.cancel(notifiable.notificationId) - } - } - - coreContext.core.removeListener(listener) - } - - @SuppressLint("MissingPermission") - private fun notify(id: Int, notification: Notification, tag: String? = null) { - if (!PermissionHelper.get().hasPostNotificationsPermission()) { - Log.w( - "[Notifications Manager] Can't notify [$id] with tag [$tag], POST_NOTIFICATIONS permission isn't granted!" - ) - return - } - - Log.i("[Notifications Manager] Notifying [$id] with tag [$tag]") - try { - notificationManager.notify(tag, id, notification) - } catch (iae: IllegalArgumentException) { - if (service == null && tag == null) { - // We can't notify using CallStyle if there isn't a foreground service running - Log.w( - "[Notifications Manager] Foreground service hasn't started yet, can't display a CallStyle notification until then: $iae" - ) - } else { - Log.e("[Notifications Manager] Exception occurred: $iae") - } - } - } - - fun cancel(id: Int, tag: String? = null) { - Log.i("[Notifications Manager] Canceling [$id] with tag [$tag]") - notificationManager.cancel(tag, id) - } - - fun resetChatNotificationCounterForSipUri(sipUri: String) { - val notifiable: Notifiable? = chatNotificationsMap[sipUri] - notifiable?.messages?.clear() - } - - /* Service related */ - - fun startForeground() { - val serviceChannel = context.getString(R.string.notification_channel_service_id) - if (Compatibility.getChannelImportance(notificationManager, serviceChannel) == NotificationManagerCompat.IMPORTANCE_NONE) { - Log.w("[Notifications Manager] Service channel is disabled!") - return - } - - val coreService = service - if (coreService != null) { - startForeground(coreService) - } else { - Log.w( - "[Notifications Manager] Can't start service as foreground without a service, starting it now" - ) - val intent = Intent() - intent.setClass(coreContext.context, CoreService::class.java) - try { - Compatibility.startForegroundService(coreContext.context, intent) - } catch (ise: IllegalStateException) { - Log.e("[Notifications Manager] Failed to start Service: $ise") - } catch (se: SecurityException) { - Log.e("[Notifications Manager] Failed to start Service: $se") - } - } - } - - fun startCallForeground(coreService: CoreService) { - service = coreService - when { - currentForegroundServiceNotificationId != 0 -> { - if (currentForegroundServiceNotificationId != SERVICE_NOTIF_ID) { - Log.e( - "[Notifications Manager] There is already a foreground service notification [$currentForegroundServiceNotificationId]" - ) - } else { - Log.i( - "[Notifications Manager] There is already a foreground service notification, no need to use the call notification to keep Service alive" - ) - } - } - coreContext.core.callsNb > 0 -> { - // When this method will be called, we won't have any notification yet - val call = coreContext.core.currentCall ?: coreContext.core.calls[0] - when (call.state) { - Call.State.IncomingReceived, Call.State.IncomingEarlyMedia -> { - Log.i( - "[Notifications Manager] Creating incoming call notification to be used as foreground service" - ) - displayIncomingCallNotification(call, true) - } - else -> { - Log.i( - "[Notifications Manager] Creating call notification to be used as foreground service" - ) - displayCallNotification(call, true) - } - } - } - } - } - - private fun startForeground(coreService: CoreService) { - service = coreService - - val notification = serviceNotification ?: createServiceNotification(false) - if (notification == null) { - Log.e( - "[Notifications Manager] Failed to create service notification, aborting foreground service!" - ) - return - } - - currentForegroundServiceNotificationId = SERVICE_NOTIF_ID - Log.i( - "[Notifications Manager] Starting service as foreground [$currentForegroundServiceNotificationId]" - ) - - val core = coreContext.core - val isActiveCall = if (core.callsNb > 0) { - val currentCall = core.currentCall ?: core.calls.first() - when (currentCall.state) { - Call.State.IncomingReceived, Call.State.IncomingEarlyMedia, Call.State.OutgoingInit, Call.State.OutgoingProgress, Call.State.OutgoingRinging -> false - else -> true - } - } else { - false - } - - Compatibility.startDataSyncForegroundService( - coreService, - currentForegroundServiceNotificationId, - notification, - isActiveCall - ) - } - - fun startForegroundToKeepAppAlive( - coreService: CoreService, - useAutoStartDescription: Boolean = true - ) { - service = coreService - - val notification = serviceNotification ?: createServiceNotification(useAutoStartDescription) - if (notification == null) { - Log.e( - "[Notifications Manager] Failed to create service notification, aborting foreground service!" - ) - return - } - - currentForegroundServiceNotificationId = SERVICE_NOTIF_ID - Log.i( - "[Notifications Manager] Starting service as foreground [$currentForegroundServiceNotificationId]" - ) - - Compatibility.startDataSyncForegroundService( - coreService, - currentForegroundServiceNotificationId, - notification, - false - ) - } - - private fun startForeground( - notificationId: Int, - callNotification: Notification, - isCallActive: Boolean - ) { - val coreService = service - if (coreService != null && (currentForegroundServiceNotificationId == 0 || currentForegroundServiceNotificationId == notificationId)) { - Log.i( - "[Notifications Manager] Starting service as foreground using call notification [$notificationId]" - ) - try { - currentForegroundServiceNotificationId = notificationId - - Compatibility.startCallForegroundService( - coreService, - currentForegroundServiceNotificationId, - callNotification, - isCallActive - ) - } catch (e: Exception) { - Log.e("[Notifications Manager] Foreground service wasn't allowed! $e") - currentForegroundServiceNotificationId = 0 - } - } else { - Log.w( - "[Notifications Manager] Can't start foreground service using notification id [$notificationId] (current foreground service notification id is [$currentForegroundServiceNotificationId]) and service [$coreService]" - ) - } - } - - fun stopForegroundNotification() { - if (service != null) { - if (currentForegroundServiceNotificationId != 0) { - Log.i( - "[Notifications Manager] Stopping service as foreground [$currentForegroundServiceNotificationId]" - ) - currentForegroundServiceNotificationId = 0 - } - service?.stopForeground(true) - } - } - - fun stopForegroundNotificationIfPossible() { - if (service != null && currentForegroundServiceNotificationId == SERVICE_NOTIF_ID && !corePreferences.keepServiceAlive) { - Log.i( - "[Notifications Manager] Stopping auto-started service notification [$currentForegroundServiceNotificationId]" - ) - stopForegroundNotification() - } - } - - fun stopCallForeground() { - if (service != null && currentForegroundServiceNotificationId != SERVICE_NOTIF_ID) { - Log.i( - "[Notifications Manager] Stopping call notification [$currentForegroundServiceNotificationId] used as foreground service" - ) - stopForegroundNotification() - } - } - - fun serviceCreated(createdService: CoreService) { - Log.i("[Notifications Manager] Service has been created, keeping it around") - service = createdService - } - - fun serviceDestroyed() { - Log.i("[Notifications Manager] Service has been destroyed") - stopForegroundNotification() - service = null - } - - private fun createServiceNotification(useAutoStartDescription: Boolean = false): Notification? { - val serviceChannel = context.getString(R.string.notification_channel_service_id) - if (Compatibility.getChannelImportance(notificationManager, serviceChannel) == NotificationManagerCompat.IMPORTANCE_NONE) { - Log.w("[Notifications Manager] Service channel is disabled!") - return null - } - - val pendingIntent = NavDeepLinkBuilder(context) - .setComponentName(MainActivity::class.java) - .setGraph(R.navigation.main_nav_graph) - .setDestination(R.id.dialerFragment) - .createPendingIntent() - - val builder = NotificationCompat.Builder(context, serviceChannel) - .setContentTitle(context.getString(R.string.service_name)) - .setContentText( - if (useAutoStartDescription) { - context.getString( - R.string.service_auto_start_description - ) - } else { - context.getString(R.string.service_description) - } - ) - .setSmallIcon(R.drawable.topbar_service_notification) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setVisibility(NotificationCompat.VISIBILITY_SECRET) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setOngoing(true) - .setColor(ContextCompat.getColor(context, R.color.primary_color)) - - if (!corePreferences.preventInterfaceFromShowingUp) { - builder.setContentIntent(pendingIntent) - } - - val notif = builder.build() - serviceNotification = notif - return notif - } - - /* Call related */ - - private fun getNotificationIdForCall(call: Call): Int { - return call.callLog.startDate.toInt() - } - - private fun getNotifiableForCall(call: Call): Notifiable { - val address = call.remoteAddress.asStringUriOnly() - var notifiable: Notifiable? = callNotificationsMap[address] - if (notifiable == null) { - notifiable = Notifiable(getNotificationIdForCall(call)) - notifiable.remoteAddress = call.remoteAddress.asStringUriOnly() - - callNotificationsMap[address] = notifiable - } - return notifiable - } - - fun getPerson(friend: Friend?, displayName: String, picture: Bitmap?): Person { - return friend?.getPerson() - ?: Person.Builder() - .setName(displayName) - .setIcon( - if (picture != null) { - IconCompat.createWithAdaptiveBitmap(picture) - } else { - coreContext.contactsManager.contactAvatar - } - ) - .setKey(displayName) - .build() - } - - fun displayIncomingCallNotification(call: Call, useAsForeground: Boolean) { - if (coreContext.declineCallDueToGsmActiveCall()) { - Log.w( - "[Notifications Manager] Call will be declined, do not show incoming call notification" - ) - return - } - - val notifiable = getNotifiableForCall(call) - if (notifiable.notificationId == currentForegroundServiceNotificationId) { - Log.i( - "[Notifications Manager] There is already a Service foreground notification for this incoming call, skipping" - ) - return - } - - try { - val showLockScreenNotification = android.provider.Settings.Secure.getInt( - context.contentResolver, - "lock_screen_show_notifications", - 0 - ) - Log.i( - "[Notifications Manager] Are notifications allowed on lock screen? ${showLockScreenNotification != 0} ($showLockScreenNotification)" - ) - } catch (e: Exception) { - Log.e( - "[Notifications Manager] Failed to get android.provider.Settings.Secure.getInt(lock_screen_show_notifications): $e" - ) - } - - val incomingCallNotificationIntent = Intent(context, CallActivity::class.java) - incomingCallNotificationIntent.addFlags( - Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_NO_USER_ACTION or Intent.FLAG_FROM_BACKGROUND - ) - val pendingIntent = PendingIntent.getActivity( - context, - 0, - incomingCallNotificationIntent, - PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - val notification = Compatibility.createIncomingCallNotification( - context, - call, - notifiable, - pendingIntent, - this - ) - Log.i( - "[Notifications Manager] Notifying incoming call notification [${notifiable.notificationId}]" - ) - notify(notifiable.notificationId, notification) - - if (useAsForeground) { - Log.i( - "[Notifications Manager] Notifying incoming call notification for foreground service [${notifiable.notificationId}]" - ) - startForeground(notifiable.notificationId, notification, false) - } - } - - private fun displayMissedCallNotification(remoteAddress: Address) { - val missedCallCount: Int = coreContext.core.missedCallsCount - val body: String - if (missedCallCount > 1) { - body = context.getString(R.string.missed_calls_notification_body) - .format(missedCallCount) - Log.i( - "[Notifications Manager] Updating missed calls notification count to $missedCallCount" - ) - } else { - val friend: Friend? = coreContext.contactsManager.findContactByAddress(remoteAddress) - body = context.getString(R.string.missed_call_notification_body) - .format(friend?.name ?: LinphoneUtils.getDisplayName(remoteAddress)) - Log.i("[Notifications Manager] Creating missed call notification") - } - - val pendingIntent = NavDeepLinkBuilder(context) - .setComponentName(MainActivity::class.java) - .setGraph(R.navigation.main_nav_graph) - .setDestination(R.id.masterCallLogsFragment) - .createPendingIntent() - - val builder = NotificationCompat.Builder( - context, - context.getString(R.string.notification_channel_missed_call_id) - ) - .setContentTitle(context.getString(R.string.missed_call_notification_title)) - .setContentText(body) - .setSmallIcon(R.drawable.topbar_missed_call_notification) - .setAutoCancel(true) - // .setCategory(NotificationCompat.CATEGORY_EVENT) No one really matches "missed call" - .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setNumber(missedCallCount) - .setColor(ContextCompat.getColor(context, R.color.notification_led_color)) - - if (!corePreferences.preventInterfaceFromShowingUp) { - builder.setContentIntent(pendingIntent) - } - - val notification = builder.build() - notify(MISSED_CALLS_NOTIF_ID, notification, MISSED_CALL_TAG) - } - - fun dismissMissedCallNotification() { - cancel(MISSED_CALLS_NOTIF_ID, MISSED_CALL_TAG) - } - - private fun displayCallNotification(call: Call, isCallActive: Boolean) { - val notifiable = getNotifiableForCall(call) - - val serviceChannel = context.getString(R.string.notification_channel_service_id) - val channelToUse = when ( - val serviceChannelImportance = Compatibility.getChannelImportance( - notificationManager, - serviceChannel - ) - ) { - NotificationManagerCompat.IMPORTANCE_NONE -> { - Log.w( - "[Notifications Manager] Service channel is disabled, using incoming call channel instead!" - ) - context.getString(R.string.notification_channel_incoming_call_id) - } - NotificationManagerCompat.IMPORTANCE_LOW -> { - // Expected, nothing to do - serviceChannel - } - else -> { - // If user disables & enabled back service notifications channel, importance won't be low anymore but default! - Log.w( - "[Notifications Manager] Service channel importance is $serviceChannelImportance and not LOW (${NotificationManagerCompat.IMPORTANCE_LOW}) as expected!" - ) - serviceChannel - } - } - - val callNotificationIntent = Intent(context, CallActivity::class.java) - callNotificationIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - val pendingIntent = PendingIntent.getActivity( - context, - 0, - callNotificationIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - - val notification = Compatibility.createCallNotification( - context, - call, - notifiable, - pendingIntent, - channelToUse, - this - ) - Log.i("[Notifications Manager] Notifying call notification [${notifiable.notificationId}]") - notify(notifiable.notificationId, notification) - - val coreService = service - if (coreService != null && (currentForegroundServiceNotificationId == 0 || currentForegroundServiceNotificationId == notifiable.notificationId)) { - Log.i( - "[Notifications Manager] Notifying call notification for foreground service [${notifiable.notificationId}]" - ) - startForeground(notifiable.notificationId, notification, isCallActive) - } else if (coreService != null && currentForegroundServiceNotificationId == SERVICE_NOTIF_ID) { - // To add microphone & camera foreground service use to foreground service if needed - startForeground(coreService) - } - } - - private fun dismissCallNotification(call: Call) { - val address = call.remoteAddress.asStringUriOnly() - val notifiable: Notifiable? = callNotificationsMap[address] - if (notifiable != null) { - cancel(notifiable.notificationId) - callNotificationsMap.remove(address) - } else { - Log.w("[Notifications Manager] No notification found for call ${call.callLog.callId}") - } - - // To remove microphone & camera foreground service use to foreground service if needed - val coreService = service - if (coreService != null && currentForegroundServiceNotificationId == SERVICE_NOTIF_ID) { - startForeground(coreService) - } - } - - /* Chat related */ - - private fun getNotificationIdForChat(chatRoom: ChatRoom): Int { - return LinphoneUtils.getChatRoomId(chatRoom).hashCode() - } - - private fun displayChatNotifiable(room: ChatRoom, notifiable: Notifiable) { - val localAddress = room.localAddress.asStringUriOnly() - val peerAddress = room.peerAddress.asStringUriOnly() - val args = Bundle() - args.putString("RemoteSipUri", peerAddress) - args.putString("LocalSipUri", localAddress) - - val pendingIntent = NavDeepLinkBuilder(context) - .setComponentName(MainActivity::class.java) - .setGraph(R.navigation.main_nav_graph) - .setDestination(R.id.masterChatRoomsFragment) - .setArguments(args) - .createPendingIntent() - - // PendingIntents attached to bubbles must be mutable - val target = Intent(context, ChatBubbleActivity::class.java) - target.putExtra("RemoteSipUri", peerAddress) - target.putExtra("LocalSipUri", localAddress) - val bubbleIntent = PendingIntent.getActivity( - context, - notifiable.notificationId, - target, - Compatibility.getUpdateCurrentPendingIntentFlag() - ) - - val id = LinphoneUtils.getChatRoomId(room.localAddress, room.peerAddress) - val me = coreContext.contactsManager.getMePerson(room.localAddress) - val notification = createMessageNotification( - notifiable, - pendingIntent, - bubbleIntent, - id, - me - ) - notify(notifiable.notificationId, notification, CHAT_TAG) - } - - private fun updateChatNotifiableWithMessages( - notifiable: Notifiable, - room: ChatRoom, - messages: Array - ): Boolean { - var updated = false - for (message in messages) { - if (message.isRead || message.isOutgoing) continue - val friend = coreContext.contactsManager.findContactByAddress(message.fromAddress) - val notifiableMessage = getNotifiableMessage(message, friend) - notifiable.messages.add(notifiableMessage) - updated = true - } - - if (room.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) { - notifiable.isGroup = false - } else { - notifiable.isGroup = true - notifiable.groupTitle = room.subject - } - return updated - } - - private fun createChatReactionNotifiable( - room: ChatRoom, - reaction: String, - from: Address, - message: ChatMessage - ): Notifiable { - val notifiable = getNotifiableForRoom(room) - - // Check for previous reaction notifiable from the same person for the same message - val fromAddr = from.asStringUriOnly() - val found = notifiable.messages.find { - it.isReaction && it.reactionToMessageId == message.messageId && it.reactionFrom == fromAddr - } - if (found != null) { - Log.i( - "[Notifications Manager] Found a previous notifiable for a reaction from the same person to the same message, removing it" - ) - notifiable.messages.remove(found) - } - - val friend = coreContext.contactsManager.findContactByAddress(from) - val roundPicture = ImageUtils.getRoundBitmapFromUri(context, friend?.getThumbnailUri()) - val displayName = friend?.name ?: LinphoneUtils.getDisplayName(from) - - val originalMessage = LinphoneUtils.getTextDescribingMessage(message) - val text = AppUtils.getString(R.string.chat_message_reaction_received).format( - displayName, - reaction, - originalMessage - ) - - val notifiableMessage = NotifiableMessage( - text, - friend, - displayName, - message.time, - senderAvatar = roundPicture, - isOutgoing = false, - isReaction = true, - reactionToMessageId = message.messageId, - reactionFrom = from.asStringUriOnly() - ) - notifiable.messages.add(notifiableMessage) - - return notifiable - } - - private fun getNotifiableForRoom(room: ChatRoom): Notifiable { - val address = room.peerAddress.asStringUriOnly() - var notifiable: Notifiable? = chatNotificationsMap[address] - if (notifiable == null) { - notifiable = Notifiable(getNotificationIdForChat(room)) - notifiable.myself = LinphoneUtils.getDisplayName(room.localAddress) - notifiable.localIdentity = room.localAddress.asStringUriOnly() - notifiable.remoteAddress = room.peerAddress.asStringUriOnly() - - chatNotificationsMap[address] = notifiable - - if (room.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) { - notifiable.isGroup = false - } else { - notifiable.isGroup = true - notifiable.groupTitle = room.subject - } - } - return notifiable - } - - private fun getNotifiableMessage(message: ChatMessage, friend: Friend?): NotifiableMessage { - val roundPicture = ImageUtils.getRoundBitmapFromUri(context, friend?.getThumbnailUri()) - val displayName = friend?.name ?: LinphoneUtils.getDisplayName(message.fromAddress) - val text = LinphoneUtils.getTextDescribingMessage(message) - val notifiableMessage = NotifiableMessage( - text, - friend, - displayName, - message.time * 1000, /* Linphone timestamps are in seconds */ - senderAvatar = roundPicture, - isOutgoing = message.isOutgoing - ) - - for (content in message.contents) { - if (content.isFile) { - val path = content.filePath - if (path != null) { - val contentUri: Uri = FileUtils.getPublicFilePath(context, path) - val filePath: String = contentUri.toString() - val extension = FileUtils.getExtensionFromFileName(filePath) - if (extension.isNotEmpty()) { - val mime = - MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - notifiableMessage.filePath = contentUri - notifiableMessage.fileMime = mime - Log.i( - "[Notifications Manager] Added file $contentUri with MIME $mime to notification" - ) - } else { - Log.e( - "[Notifications Manager] Couldn't find extension for incoming message with file $path" - ) - } - } - } - } - - return notifiableMessage - } - - private fun displayReplyMessageNotification(message: ChatMessage, notifiable: Notifiable) { - Log.i( - "[Notifications Manager] Updating message notification with reply for notification ${notifiable.notificationId}" - ) - - val text = message.contents.find { content -> content.isText }?.utf8Text ?: "" - val senderAddress = message.fromAddress - val reply = NotifiableMessage( - text, - null, - notifiable.myself ?: LinphoneUtils.getDisplayName(senderAddress), - System.currentTimeMillis(), - isOutgoing = true - ) - notifiable.messages.add(reply) - - displayChatNotifiable(message.chatRoom, notifiable) - } - - fun dismissChatNotification(room: ChatRoom): Boolean { - val address = room.peerAddress.asStringUriOnly() - val notifiable: Notifiable? = chatNotificationsMap[address] - if (notifiable != null) { - Log.i( - "[Notifications Manager] Dismissing notification for chat room $room with id ${notifiable.notificationId}" - ) - notifiable.messages.clear() - cancel(notifiable.notificationId, CHAT_TAG) - return true - } else { - val previousNotificationId = previousChatNotifications.find { id -> - id == getNotificationIdForChat( - room - ) - } - if (previousNotificationId != null) { - if (chatBubbleNotifications.contains(previousNotificationId)) { - Log.i( - "[Notifications Manager] Found previous notification with same ID [$previousNotificationId] but not cancelling it as it's ID is in chat bubbles list" - ) - } else { - Log.i( - "[Notifications Manager] Found previous notification with same ID [$previousNotificationId], canceling it" - ) - cancel(previousNotificationId, CHAT_TAG) - } - return true - } - } - return false - } - - fun changeDismissNotificationUponReadForChatRoom(chatRoom: ChatRoom, dismiss: Boolean) { - val notificationId = getNotificationIdForChat(chatRoom) - if (dismiss) { - Log.i( - "[Notifications Manager] Allow notification with id [$notificationId] to be dismissed when chat room will be marked as read, used for chat bubble" - ) - chatBubbleNotifications.add(notificationId) - } else { - Log.i( - "[Notifications Manager] Prevent notification with id [$notificationId] from being dismissed when chat room will be marked as read, used for chat bubble" - ) - chatBubbleNotifications.remove(notificationId) - } - } - - /* Notifications */ - - private fun createMessageNotification( - notifiable: Notifiable, - pendingIntent: PendingIntent, - bubbleIntent: PendingIntent, - id: String, - me: Person - ): Notification { - val style = NotificationCompat.MessagingStyle(me) - val allPersons = arrayListOf() - - var lastPersonAvatar: Bitmap? = null - var lastPerson: Person? = null - for (message in notifiable.messages) { - val friend = message.friend - val person = getPerson(friend, message.sender, message.senderAvatar) - - if (!message.isOutgoing) { - // We don't want to see our own avatar - lastPerson = person - lastPersonAvatar = message.senderAvatar - - if (allPersons.find { it.key == person.key } == null) { - allPersons.add(person) - } - } - - val senderPerson = if (message.isOutgoing) null else person // Use null for ourselves - val msg = if (corePreferences.hideChatMessageContentInNotification) { - NotificationCompat.MessagingStyle.Message( - AppUtils.getString(R.string.chat_message_notification_hidden_content), - message.time, - senderPerson - ) - } else { - val tmp = NotificationCompat.MessagingStyle.Message( - message.message, - message.time, - senderPerson - ) - if (message.filePath != null) tmp.setData(message.fileMime, message.filePath) - tmp - } - - style.addMessage(msg) - if (message.isOutgoing) { - style.addHistoricMessage(msg) - } - } - - style.conversationTitle = if (notifiable.isGroup) notifiable.groupTitle else lastPerson?.name - style.isGroupConversation = notifiable.isGroup - - val icon = lastPerson?.icon ?: coreContext.contactsManager.contactAvatar - val bubble = NotificationCompat.BubbleMetadata.Builder(bubbleIntent, icon) - .setDesiredHeightResId(R.dimen.chat_message_bubble_desired_height) - .build() - - val largeIcon = if (notifiable.isGroup) coreContext.contactsManager.groupBitmap else lastPersonAvatar - val notificationBuilder = NotificationCompat.Builder( - context, - context.getString(R.string.notification_channel_chat_id) - ) - .setSmallIcon(R.drawable.topbar_chat_notification) - .setAutoCancel(true) - .setLargeIcon(largeIcon) - .setColor(ContextCompat.getColor(context, R.color.primary_color)) - .setCategory(NotificationCompat.CATEGORY_MESSAGE) - .setGroup(CHAT_NOTIFICATIONS_GROUP) - .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) - .setNumber(notifiable.messages.size) - .setWhen(System.currentTimeMillis()) - .setShowWhen(true) - .setStyle(style) - .addAction(getReplyMessageAction(notifiable)) - .addAction(getMarkMessageAsReadAction(notifiable)) - .setShortcutId(id) - .setLocusId(LocusIdCompat(id)) - - for (person in allPersons) { - notificationBuilder.addPerson(person) - } - - if (!corePreferences.preventInterfaceFromShowingUp) { - notificationBuilder.setContentIntent(pendingIntent) - } - - if (corePreferences.markAsReadUponChatMessageNotificationDismissal) { - Log.i( - "[Notifications Manager] Chat room will be marked as read when notification will be dismissed" - ) - notificationBuilder - .setDeleteIntent(getMarkMessageAsReadPendingIntent(notifiable)) - } - - if (!Compatibility.canChatMessageChannelBubble(context)) { - Log.w("[Notifications Manager] This conversation wasn't granted bubble permission yet") - } - // We still need to set the bubbleMetadata, otherwise user won't ever be able to enable bubbles! - notificationBuilder.bubbleMetadata = bubble - return notificationBuilder.build() - } - - /* Notifications actions */ - - fun getCallAnswerPendingIntent(notifiable: Notifiable): PendingIntent { - val answerIntent = Intent(context, NotificationBroadcastReceiver::class.java) - answerIntent.action = INTENT_ANSWER_CALL_NOTIF_ACTION - answerIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) - answerIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) - - return PendingIntent.getBroadcast( - context, - notifiable.notificationId, - answerIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } - - fun getCallAnswerAction(notifiable: Notifiable): NotificationCompat.Action { - return NotificationCompat.Action.Builder( - R.drawable.call_audio_start, - context.getString(R.string.incoming_call_notification_answer_action_label), - getCallAnswerPendingIntent(notifiable) - ).build() - } - - fun getCallDeclinePendingIntent(notifiable: Notifiable): PendingIntent { - val hangupIntent = Intent(context, NotificationBroadcastReceiver::class.java) - hangupIntent.action = INTENT_HANGUP_CALL_NOTIF_ACTION - hangupIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) - hangupIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) - - return PendingIntent.getBroadcast( - context, - notifiable.notificationId, - hangupIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } - - fun getCallDeclineAction(notifiable: Notifiable): NotificationCompat.Action { - return NotificationCompat.Action.Builder( - R.drawable.call_hangup, - context.getString(R.string.incoming_call_notification_hangup_action_label), - getCallDeclinePendingIntent(notifiable) - ) - .setShowsUserInterface(false) - .build() - } - - private fun getReplyMessageAction(notifiable: Notifiable): NotificationCompat.Action { - val replyLabel = - context.resources.getString(R.string.received_chat_notification_reply_label) - val remoteInput = - RemoteInput.Builder(KEY_TEXT_REPLY).setLabel(replyLabel).build() - - val replyIntent = Intent(context, NotificationBroadcastReceiver::class.java) - replyIntent.action = INTENT_REPLY_NOTIF_ACTION - replyIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) - replyIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) - replyIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) - - // PendingIntents attached to actions with remote inputs must be mutable - val replyPendingIntent = PendingIntent.getBroadcast( - context, - notifiable.notificationId, - replyIntent, - Compatibility.getUpdateCurrentPendingIntentFlag() - ) - return NotificationCompat.Action.Builder( - R.drawable.chat_send_over, - context.getString(R.string.received_chat_notification_reply_label), - replyPendingIntent - ) - .addRemoteInput(remoteInput) - .setAllowGeneratedReplies(true) - .setShowsUserInterface(false) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) - .build() - } - - private fun getMarkMessageAsReadPendingIntent(notifiable: Notifiable): PendingIntent { - val markAsReadIntent = Intent(context, NotificationBroadcastReceiver::class.java) - markAsReadIntent.action = INTENT_MARK_AS_READ_ACTION - markAsReadIntent.putExtra(INTENT_NOTIF_ID, notifiable.notificationId) - markAsReadIntent.putExtra(INTENT_LOCAL_IDENTITY, notifiable.localIdentity) - markAsReadIntent.putExtra(INTENT_REMOTE_ADDRESS, notifiable.remoteAddress) - - return PendingIntent.getBroadcast( - context, - notifiable.notificationId, - markAsReadIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - } - - private fun getMarkMessageAsReadAction(notifiable: Notifiable): NotificationCompat.Action { - val markAsReadPendingIntent = getMarkMessageAsReadPendingIntent(notifiable) - return NotificationCompat.Action.Builder( - R.drawable.chat_send_over, - context.getString(R.string.received_chat_notification_mark_as_read_label), - markAsReadPendingIntent - ) - .setShowsUserInterface(false) - .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_MARK_AS_READ) - .build() - } -} diff --git a/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt b/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt deleted file mode 100644 index 823490e32..000000000 --- a/app/src/main/java/org/linphone/telecom/NativeCallWrapper.kt +++ /dev/null @@ -1,178 +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 . - */ -package org.linphone.telecom - -import android.annotation.TargetApi -import android.graphics.drawable.Icon -import android.os.Bundle -import android.telecom.CallAudioState -import android.telecom.Connection -import android.telecom.DisconnectCause -import android.telecom.StatusHints -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.core.Call -import org.linphone.core.tools.Log -import org.linphone.utils.AudioRouteUtils - -@TargetApi(29) -class NativeCallWrapper(var callId: String) : Connection() { - init { - val properties = connectionProperties or PROPERTY_SELF_MANAGED - connectionProperties = properties - - val capabilities = connectionCapabilities or CAPABILITY_MUTE or CAPABILITY_SUPPORT_HOLD or CAPABILITY_HOLD - connectionCapabilities = capabilities - - audioModeIsVoip = true - statusHints = StatusHints( - "", - Icon.createWithResource(coreContext.context, R.drawable.linphone_logo_tinted), - Bundle() - ) - } - - override fun onStateChanged(state: Int) { - Log.i( - "[Connection] Telecom state changed [${intStateToString(state)}] for call with id: $callId" - ) - super.onStateChanged(state) - } - - override fun onAnswer(videoState: Int) { - Log.i("[Connection] Answering telecom call with id: $callId") - getCall()?.accept() ?: selfDestroy() - } - - override fun onHold() { - Log.i("[Connection] Pausing telecom call with id: $callId") - getCall()?.let { call -> - if (call.conference != null) { - call.conference?.leave() - } else { - call.pause() - } - } ?: selfDestroy() - setOnHold() - } - - override fun onUnhold() { - Log.i("[Connection] Resuming telecom call with id: $callId") - getCall()?.let { call -> - if (call.conference != null) { - call.conference?.enter() - } else { - call.resume() - } - } ?: selfDestroy() - setActive() - } - - override fun onCallAudioStateChanged(state: CallAudioState) { - Log.i("[Connection] Audio state changed: $state") - - val call = getCall() - if (call != null) { - if (getState() != STATE_ACTIVE && getState() != STATE_DIALING) { - Log.w( - "[Connection] Call state isn't STATE_ACTIVE or STATE_DIALING, ignoring mute mic & audio route directive from TelecomManager" - ) - return - } - - if (state.isMuted != call.microphoneMuted) { - Log.w( - "[Connection] Connection audio state asks for changing in mute: ${state.isMuted}, currently is ${call.microphoneMuted}" - ) - if (state.isMuted) { - Log.w("[Connection] Muting microphone") - call.microphoneMuted = true - } - } - - when (state.route) { - CallAudioState.ROUTE_EARPIECE -> AudioRouteUtils.routeAudioToEarpiece(call, true) - CallAudioState.ROUTE_SPEAKER -> AudioRouteUtils.routeAudioToSpeaker(call, true) - CallAudioState.ROUTE_BLUETOOTH -> AudioRouteUtils.routeAudioToBluetooth(call, true) - CallAudioState.ROUTE_WIRED_HEADSET -> AudioRouteUtils.routeAudioToHeadset( - call, - true - ) - } - } else { - selfDestroy() - } - } - - override fun onPlayDtmfTone(c: Char) { - Log.i("[Connection] Sending DTMF [$c] in telecom call with id: $callId") - getCall()?.sendDtmf(c) ?: selfDestroy() - } - - override fun onDisconnect() { - Log.i("[Connection] Terminating telecom call with id: $callId") - getCall()?.terminate() ?: selfDestroy() - } - - override fun onAbort() { - Log.i("[Connection] Aborting telecom call with id: $callId") - getCall()?.terminate() ?: selfDestroy() - } - - override fun onReject() { - Log.i("[Connection] Rejecting telecom call with id: $callId") - getCall()?.terminate() ?: selfDestroy() - } - - override fun onSilence() { - Log.i("[Connection] Call with id: $callId asked to be silenced") - coreContext.core.stopRinging() - } - - fun stateAsString(): String { - return stateToString(state) - } - - private fun getCall(): Call? { - return coreContext.core.getCallByCallid(callId) - } - - private fun selfDestroy() { - if (coreContext.core.callsNb == 0) { - Log.e("[Connection] No call in Core, destroy connection") - setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) - destroy() - } - } - - private fun intStateToString(state: Int): String { - return when (state) { - STATE_INITIALIZING -> "STATE_INITIALIZING" - STATE_NEW -> "STATE_NEW" - STATE_RINGING -> "STATE_RINGING" - STATE_DIALING -> "STATE_DIALING" - STATE_ACTIVE -> "STATE_ACTIVE" - STATE_HOLDING -> "STATE_HOLDING" - STATE_DISCONNECTED -> "STATE_DISCONNECTED" - STATE_PULLING_CALL -> "STATE_PULLING_CALL" - else -> "STATE_UNKNOWN" - } - } -} diff --git a/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt b/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt deleted file mode 100644 index fddfc8bb6..000000000 --- a/app/src/main/java/org/linphone/telecom/TelecomConnectionService.kt +++ /dev/null @@ -1,299 +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 . - */ -package org.linphone.telecom - -import android.annotation.TargetApi -import android.content.ComponentName -import android.content.Intent -import android.net.Uri -import android.telecom.* -import org.linphone.LinphoneApplication -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.ensureCoreExists -import org.linphone.core.Call -import org.linphone.core.Core -import org.linphone.core.CoreListenerStub -import org.linphone.core.tools.Log - -@TargetApi(29) -class TelecomConnectionService : ConnectionService() { - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State?, - message: String - ) { - Log.i( - "[Telecom Connection Service] call [${call.callLog.callId}] state changed: $state" - ) - when (call.state) { - Call.State.OutgoingProgress -> { - for (connection in TelecomHelper.get().connections) { - if (connection.callId.isEmpty()) { - Log.i( - "[Telecom Connection Service] Updating connection with call ID: ${call.callLog.callId}" - ) - connection.callId = core.currentCall?.callLog?.callId ?: "" - } - } - } - Call.State.Error -> onCallError(call) - Call.State.End, Call.State.Released -> onCallEnded(call) - Call.State.Paused, Call.State.Pausing, Call.State.PausedByRemote -> onCallPaused( - call - ) - Call.State.Connected, Call.State.StreamsRunning -> onCallConnected(call) - else -> {} - } - } - - override fun onLastCallEnded(core: Core) { - val connectionsCount = TelecomHelper.get().connections.size - if (connectionsCount > 0) { - Log.w( - "[Telecom Connection Service] Last call ended, there is $connectionsCount connections still alive" - ) - for (connection in TelecomHelper.get().connections) { - Log.w( - "[Telecom Connection Service] Destroying zombie connection ${connection.callId}" - ) - connection.setDisconnected(DisconnectCause(DisconnectCause.OTHER)) - connection.destroy() - } - } - } - } - - override fun onCreate() { - super.onCreate() - - Log.i("[Telecom Connection Service] onCreate()") - ensureCoreExists(applicationContext) - coreContext.core.addListener(listener) - } - - override fun onUnbind(intent: Intent?): Boolean { - if (LinphoneApplication.contextExists()) { - Log.i("[Telecom Connection Service] onUnbind()") - coreContext.core.removeListener(listener) - } - - return super.onUnbind(intent) - } - - override fun onCreateOutgoingConnection( - connectionManagerPhoneAccount: PhoneAccountHandle?, - request: ConnectionRequest - ): Connection { - if (coreContext.core.callsNb == 0) { - Log.w("[Telecom Connection Service] No call in Core, aborting outgoing connection!") - return Connection.createCanceledConnection() - } - - val accountHandle = request.accountHandle - val componentName = ComponentName(applicationContext, this.javaClass) - return if (accountHandle != null && componentName == accountHandle.componentName) { - Log.i("[Telecom Connection Service] Creating outgoing connection") - - val extras = request.extras - var callId = extras.getString("Call-ID") - val displayName = extras.getString("DisplayName") - if (callId == null) { - callId = coreContext.core.currentCall?.callLog?.callId ?: "" - } - Log.i( - "[Telecom Connection Service] Outgoing connection is for call [$callId] with display name [$displayName]" - ) - - // Prevents user dialing back from native dialer app history - if (callId.isEmpty() && displayName.isNullOrEmpty()) { - Log.e( - "[Telecom Connection Service] Looks like a call was made from native dialer history, aborting" - ) - return Connection.createFailedConnection(DisconnectCause(DisconnectCause.OTHER)) - } - - val connection = NativeCallWrapper(callId) - val call = coreContext.core.calls.find { it.callLog.callId == callId } - if (call != null) { - val callState = call.state - Log.i( - "[Telecom Connection Service] Found outgoing call from ID [$callId] with state [$callState]" - ) - when (callState) { - Call.State.OutgoingEarlyMedia, Call.State.OutgoingInit, Call.State.OutgoingProgress, Call.State.OutgoingRinging -> connection.setDialing() - Call.State.Paused, Call.State.PausedByRemote, Call.State.Pausing -> connection.setOnHold() - Call.State.End, Call.State.Error, Call.State.Released -> connection.setDisconnected( - DisconnectCause(DisconnectCause.ERROR) - ) - else -> connection.setActive() - } - } else { - Log.w( - "[Telecom Connection Service] Outgoing call not found for cal ID [$callId], assuming it's state is dialing" - ) - connection.setDialing() - } - - val providedHandle = request.address - connection.setAddress(providedHandle, TelecomManager.PRESENTATION_ALLOWED) - connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED) - Log.i("[Telecom Connection Service] Address is $providedHandle") - - TelecomHelper.get().connections.add(connection) - connection - } else { - Log.e("[Telecom Connection Service] Error: $accountHandle $componentName") - Connection.createFailedConnection( - DisconnectCause( - DisconnectCause.ERROR, - "Invalid inputs: $accountHandle $componentName" - ) - ) - } - } - - override fun onCreateIncomingConnection( - connectionManagerPhoneAccount: PhoneAccountHandle?, - request: ConnectionRequest - ): Connection { - if (coreContext.core.callsNb == 0) { - Log.w("[Telecom Connection Service] No call in Core, aborting incoming connection!") - return Connection.createCanceledConnection() - } - - val accountHandle = request.accountHandle - val componentName = ComponentName(applicationContext, this.javaClass) - return if (accountHandle != null && componentName == accountHandle.componentName) { - Log.i("[Telecom Connection Service] Creating incoming connection") - - val extras = request.extras - val incomingExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS) - var callId = incomingExtras?.getString("Call-ID") - val displayName = incomingExtras?.getString("DisplayName") - if (callId == null) { - callId = coreContext.core.currentCall?.callLog?.callId ?: "" - } - Log.i( - "[Telecom Connection Service] Incoming connection is for call [$callId] with display name [$displayName]" - ) - - val connection = NativeCallWrapper(callId) - val call = coreContext.core.calls.find { it.callLog.callId == callId } - if (call != null) { - val callState = call.state - Log.i( - "[Telecom Connection Service] Found incoming call from ID [$callId] with state [$callState]" - ) - when (callState) { - Call.State.IncomingEarlyMedia, Call.State.IncomingReceived -> connection.setRinging() - Call.State.Paused, Call.State.PausedByRemote, Call.State.Pausing -> connection.setOnHold() - Call.State.End, Call.State.Error, Call.State.Released -> connection.setDisconnected( - DisconnectCause(DisconnectCause.ERROR) - ) - else -> connection.setActive() - } - } else { - Log.w( - "[Telecom Connection Service] Incoming call not found for cal ID [$callId], assuming it's state is ringing" - ) - connection.setRinging() - } - - val providedHandle = - incomingExtras?.getParcelable(TelecomManager.EXTRA_INCOMING_CALL_ADDRESS) - connection.setAddress(providedHandle, TelecomManager.PRESENTATION_ALLOWED) - connection.setCallerDisplayName(displayName, TelecomManager.PRESENTATION_ALLOWED) - Log.i("[Telecom Connection Service] Address is $providedHandle") - - TelecomHelper.get().connections.add(connection) - connection - } else { - Log.e("[Telecom Connection Service] Error: $accountHandle $componentName") - Connection.createFailedConnection( - DisconnectCause( - DisconnectCause.ERROR, - "Invalid inputs: $accountHandle $componentName" - ) - ) - } - } - - private fun onCallError(call: Call) { - val callId = call.callLog.callId - val connection = TelecomHelper.get().findConnectionForCallId(callId.orEmpty()) - if (connection == null) { - Log.e("[Telecom Connection Service] Failed to find connection for call id: $callId") - return - } - - TelecomHelper.get().connections.remove(connection) - Log.i( - "[Telecom Connection Service] Call [$callId] is in error, destroying connection currently in ${connection.stateAsString()}" - ) - connection.setDisconnected(DisconnectCause(DisconnectCause.ERROR)) - connection.destroy() - } - - private fun onCallEnded(call: Call) { - val callId = call.callLog.callId - val connection = TelecomHelper.get().findConnectionForCallId(callId.orEmpty()) - if (connection == null) { - Log.e("[Telecom Connection Service] Failed to find connection for call id: $callId") - return - } - - TelecomHelper.get().connections.remove(connection) - val reason = call.reason - Log.i( - "[Telecom Connection Service] Call [$callId] ended with reason: $reason, destroying connection currently in ${connection.stateAsString()}" - ) - connection.setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) - connection.destroy() - } - - private fun onCallPaused(call: Call) { - val callId = call.callLog.callId - val connection = TelecomHelper.get().findConnectionForCallId(callId.orEmpty()) - if (connection == null) { - Log.e("[Telecom Connection Service] Failed to find connection for call id: $callId") - return - } - Log.i( - "[Telecom Connection Service] Setting connection as on hold, currently in ${connection.stateAsString()}" - ) - connection.setOnHold() - } - - private fun onCallConnected(call: Call) { - val callId = call.callLog.callId - val connection = TelecomHelper.get().findConnectionForCallId(callId.orEmpty()) - if (connection == null) { - Log.e("[Telecom Connection Service] Failed to find connection for call id: $callId") - return - } - - Log.i( - "[Telecom Connection Service] Setting connection as active, currently in ${connection.stateAsString()}" - ) - connection.setActive() - } -} diff --git a/app/src/main/java/org/linphone/telecom/TelecomHelper.kt b/app/src/main/java/org/linphone/telecom/TelecomHelper.kt deleted file mode 100644 index 3f55f3a89..000000000 --- a/app/src/main/java/org/linphone/telecom/TelecomHelper.kt +++ /dev/null @@ -1,251 +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 . - */ -package org.linphone.telecom - -import android.annotation.SuppressLint -import android.annotation.TargetApi -import android.content.ComponentName -import android.content.Context -import android.graphics.drawable.Icon -import android.net.Uri -import android.os.Bundle -import android.telecom.PhoneAccount -import android.telecom.PhoneAccountHandle -import android.telecom.TelecomManager -import android.telecom.TelecomManager.* -import kotlin.Exception -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.core.Call -import org.linphone.core.Core -import org.linphone.core.CoreListenerStub -import org.linphone.core.tools.Log -import org.linphone.utils.LinphoneUtils -import org.linphone.utils.PermissionHelper -import org.linphone.utils.SingletonHolder - -@TargetApi(29) -class TelecomHelper private constructor(context: Context) { - companion object : SingletonHolder(::TelecomHelper) - - private val telecomManager: TelecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager - - private var account: PhoneAccount = initPhoneAccount(context) - - val connections = arrayListOf() - - private val listener: CoreListenerStub = object : CoreListenerStub() { - override fun onCallStateChanged( - core: Core, - call: Call, - state: Call.State?, - message: String - ) { - Log.i("[Telecom Helper] Call state changed: ${call.state}") - - try { - if (call.dir == Call.Dir.Incoming && call.state == Call.State.IncomingReceived) { - onIncomingCall(call) - } else if (call.dir == Call.Dir.Outgoing && call.state == Call.State.OutgoingProgress) { - onOutgoingCall(call) - } - } catch (se: SecurityException) { - Log.e("[Telecom Helper] Exception while trying to place call: $se") - } - } - } - - init { - coreContext.core.addListener(listener) - Log.i("[Telecom Helper] Created") - } - - fun destroy() { - coreContext.core.removeListener(listener) - Log.i("[Telecom Helper] Destroyed") - } - - fun isIncomingCallPermitted(): Boolean { - val incomingCallPermitted = telecomManager.isIncomingCallPermitted(account.accountHandle) - Log.i("[Telecom Helper] Is incoming call permitted? $incomingCallPermitted") - return incomingCallPermitted - } - - @SuppressLint("MissingPermission") - fun isInManagedCall(): Boolean { - // Don't use telecomManager.isInCall as our own self-managed calls will be considered! - val isInManagedCall = telecomManager.isInManagedCall - Log.i("[Telecom Helper] Is in managed call? $isInManagedCall") - return isInManagedCall - } - - fun isAccountEnabled(): Boolean { - val enabled = account.isEnabled - Log.i("[Telecom Helper] Is account enabled ? $enabled") - return enabled - } - - @SuppressLint("MissingPermission") - fun findExistingAccount(context: Context): PhoneAccount? { - if (PermissionHelper.required(context).hasReadPhoneStateOrPhoneNumbersPermission()) { - try { - var account: PhoneAccount? = null - val phoneAccountHandleList: List = - telecomManager.selfManagedPhoneAccounts - val connectionService = ComponentName(context, TelecomConnectionService::class.java) - for (phoneAccountHandle in phoneAccountHandleList) { - val phoneAccount: PhoneAccount = - telecomManager.getPhoneAccount(phoneAccountHandle) - if (phoneAccountHandle.componentName == connectionService) { - Log.i("[Telecom Helper] Found existing phone account: $phoneAccount") - account = phoneAccount - break - } - } - if (account == null) { - Log.w("[Telecom Helper] Existing phone account not found") - } - return account - } catch (se: SecurityException) { - Log.w("[Telecom Helper] Can't check phone accounts: $se") - } - } else { - Log.e("[Telecom Helper] Can't search for existing phone account, missing permission(s)") - } - return null - } - - fun updateAccount(newAccount: PhoneAccount?) { - if (newAccount != null) { - Log.i("[Telecom Helper] Updating account object: $newAccount") - account = newAccount - } - } - - fun removeAccount() { - if (account.isEnabled) { - Log.w("[Telecom Helper] Unregistering phone account handler from telecom manager") - telecomManager.unregisterPhoneAccount(account.accountHandle) - } else { - Log.w("[Telecom Helper] Account wasn't enabled, skipping...") - } - } - - fun findConnectionForCallId(callId: String): NativeCallWrapper? { - return connections.find { connection -> - connection.callId == callId - } - } - - private fun initPhoneAccount(context: Context): PhoneAccount { - val account: PhoneAccount? = findExistingAccount(context) - if (account == null) { - Log.i("[Telecom Helper] Phone account not found, let's create it") - return createAccount(context) - } - return account - } - - private fun createAccount(context: Context): PhoneAccount { - val accountHandle = PhoneAccountHandle( - ComponentName(context, TelecomConnectionService::class.java), - context.packageName - ) - // Take care that identity may be parsed, otherwise Android OS may crash during startup - // and user will have to do a factory reset... - val identity = coreContext.core.defaultAccount?.params?.identityAddress?.asStringUriOnly() - ?: coreContext.core.createPrimaryContactParsed()?.asStringUriOnly() - ?: "sip:linphone.android@sip.linphone.org" - - val address = Uri.parse(identity) - ?: throw Exception("[Telecom Helper] Identity address for phone account is null!") - val account = PhoneAccount.builder(accountHandle, context.getString(R.string.app_name)) - .setAddress(address) - .setIcon(Icon.createWithResource(context, R.drawable.linphone_logo_tinted)) - .setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED) - .setHighlightColor(context.getColor(R.color.primary_color)) - .setShortDescription(context.getString(R.string.app_description)) - .setSupportedUriSchemes(listOf(PhoneAccount.SCHEME_SIP)) - .build() - - try { - telecomManager.registerPhoneAccount(account) - Log.i("[Telecom Helper] Phone account created: $account") - } catch (uoe: UnsupportedOperationException) { - Log.e("[Telecom Helper] Unsupported Operation Exception: $uoe") - } catch (e: Exception) { - Log.e("[Telecom Helper] Exception: $e") - } - return account - } - - private fun onIncomingCall(call: Call) { - Log.i( - "[Telecom Helper] Incoming call received from ${call.remoteAddress.asStringUriOnly()}, using account handle ${account.accountHandle}" - ) - - val extras = prepareBundle(call) - telecomManager.addNewIncomingCall( - account.accountHandle, - Bundle().apply { - putBundle(EXTRA_INCOMING_CALL_EXTRAS, extras) - putParcelable(EXTRA_PHONE_ACCOUNT_HANDLE, account.accountHandle) - } - ) - } - - @SuppressLint("MissingPermission") - private fun onOutgoingCall(call: Call) { - Log.i( - "[Telecom Helper] Outgoing call started to ${call.remoteAddress.asStringUriOnly()}, using account handle ${account.accountHandle}" - ) - - val extras = prepareBundle(call) - telecomManager.placeCall( - Uri.parse(call.remoteAddress.asStringUriOnly()), - Bundle().apply { - putBundle(EXTRA_OUTGOING_CALL_EXTRAS, extras) - putParcelable(EXTRA_PHONE_ACCOUNT_HANDLE, account.accountHandle) - } - ) - } - - private fun prepareBundle(call: Call): Bundle { - val extras = Bundle() - val address = call.remoteAddress - - if (call.dir == Call.Dir.Outgoing) { - extras.putString( - EXTRA_CALL_BACK_NUMBER, - call.remoteAddress.asStringUriOnly() - ) - } else { - extras.putParcelable(EXTRA_INCOMING_CALL_ADDRESS, Uri.parse(address.asStringUriOnly())) - } - - extras.putString("Call-ID", call.callLog.callId) - - val contact = coreContext.contactsManager.findContactByAddress(call.remoteAddress) - val displayName = contact?.name ?: LinphoneUtils.getDisplayName(call.remoteAddress) - extras.putString("DisplayName", displayName) - - return extras - } -} diff --git a/app/src/main/java/org/linphone/utils/ActivityMonitor.kt b/app/src/main/java/org/linphone/utils/ActivityMonitor.kt deleted file mode 100644 index 20f4fe482..000000000 --- a/app/src/main/java/org/linphone/utils/ActivityMonitor.kt +++ /dev/null @@ -1,121 +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 . - */ -package org.linphone.utils - -import android.app.Activity -import android.app.Application.ActivityLifecycleCallbacks -import android.os.Bundle -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.tools.service.AndroidDispatcher -import org.linphone.core.tools.service.CoreManager - -class ActivityMonitor : ActivityLifecycleCallbacks { - private val activities = ArrayList() - private var mActive = false - private var mRunningActivities = 0 - private var mLastChecker: InactivityChecker? = null - - @Synchronized - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - if (!activities.contains(activity)) activities.add(activity) - } - - override fun onActivityStarted(activity: Activity) { - } - - @Synchronized - override fun onActivityResumed(activity: Activity) { - if (!activities.contains(activity)) { - activities.add(activity) - } - mRunningActivities++ - checkActivity() - } - - @Synchronized - override fun onActivityPaused(activity: Activity) { - if (!activities.contains(activity)) { - activities.add(activity) - } else { - mRunningActivities-- - checkActivity() - } - } - - override fun onActivityStopped(activity: Activity) { - } - - @Synchronized - override fun onActivityDestroyed(activity: Activity) { - activities.remove(activity) - } - - private fun startInactivityChecker() { - if (mLastChecker != null) mLastChecker!!.cancel() - AndroidDispatcher.dispatchOnUIThreadAfter( - InactivityChecker().also { mLastChecker = it }, - 2000 - ) - } - - private fun checkActivity() { - if (mRunningActivities == 0) { - if (mActive) startInactivityChecker() - } else if (mRunningActivities > 0) { - if (!mActive) { - mActive = true - onForegroundMode() - } - if (mLastChecker != null) { - mLastChecker!!.cancel() - mLastChecker = null - } - } - } - - private fun onBackgroundMode() { - coreContext.onBackground() - } - - private fun onForegroundMode() { - coreContext.onForeground() - } - - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} - internal inner class InactivityChecker : Runnable { - private var isCanceled = false - fun cancel() { - isCanceled = true - } - - override fun run() { - if (CoreManager.isReady()) { - synchronized(CoreManager.instance()) { - if (!isCanceled) { - if (mRunningActivities == 0 && mActive) { - mActive = false - onBackgroundMode() - } - } - } - } - } - } -} diff --git a/app/src/main/java/org/linphone/utils/AppUtils.kt b/app/src/main/java/org/linphone/utils/AppUtils.kt deleted file mode 100644 index 88d61b66e..000000000 --- a/app/src/main/java/org/linphone/utils/AppUtils.kt +++ /dev/null @@ -1,255 +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 . - */ -package org.linphone.utils - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.media.AudioManager -import android.os.Bundle -import android.text.format.Formatter.formatShortFileSize -import android.util.TypedValue -import androidx.core.content.res.ResourcesCompat -import androidx.emoji2.text.EmojiCompat -import androidx.media.AudioAttributesCompat -import androidx.media.AudioFocusRequestCompat -import androidx.media.AudioManagerCompat -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import java.util.* -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.R -import org.linphone.activities.main.viewmodels.SharedMainViewModel -import org.linphone.core.tools.Log - -/** - * Various utility methods for application - */ -class AppUtils { - interface KeyboardVisibilityListener { - fun onKeyboardVisibilityChanged(visible: Boolean) - } - - companion object { - private val emojiCompat: EmojiCompat? - get() = initEmojiCompat() - - private fun initEmojiCompat(): EmojiCompat? { - return try { - EmojiCompat.get() - } catch (ise: IllegalStateException) { - Log.w( - "[App Utils] EmojiCompat.get() triggered IllegalStateException [$ise], trying manual init" - ) - EmojiCompat.init(coreContext.context) - } - } - - fun getString(id: Int): String { - return coreContext.context.getString(id) - } - - fun getStringWithPlural(id: Int, count: Int): String { - return coreContext.context.resources.getQuantityString(id, count, count) - } - - fun getStringWithPlural(id: Int, count: Int, value: String): String { - return coreContext.context.resources.getQuantityString(id, count, value) - } - - fun getDimension(id: Int): Float { - return coreContext.context.resources.getDimension(id) - } - - fun getInitials(displayName: String, limit: Int = 2): String { - if (displayName.isEmpty()) return "" - - val split = displayName.uppercase(Locale.getDefault()).split(" ") - var initials = "" - var characters = 0 - val emoji = emojiCompat - - for (i in split.indices) { - if (split[i].isNotEmpty()) { - try { - if (emoji?.loadState == EmojiCompat.LOAD_STATE_SUCCEEDED && emoji.hasEmojiGlyph( - split[i] - ) - ) { - val glyph = emoji.process(split[i]) - if (characters > 0) { // Limit initial to 1 emoji only - Log.d("[App Utils] We limit initials to one emoji only") - initials = "" - } - initials += glyph - break // Limit initial to 1 emoji only - } else { - initials += split[i][0] - } - } catch (ise: IllegalStateException) { - Log.e("[App Utils] Can't call hasEmojiGlyph: $ise") - initials += split[i][0] - } - - characters += 1 - if (characters >= limit) break - } - } - return initials - } - - fun isTextOnlyContainingEmoji(text: String): Boolean { - val emoji = emojiCompat - emoji ?: return false - - if (emoji.loadState != EmojiCompat.LOAD_STATE_SUCCEEDED) { - Log.w( - "[App Utils] Can't check emoji presence in text due to EmojiCompat library not loaded yet [${emoji.loadState}]" - ) - return false - } - - try { - for (split in text.split(" ")) { - // We only check the first and last chars of the split for commodity - if (emoji.getEmojiStart(split, 0) == -1 || - emoji.getEmojiEnd(split, split.length - 1) == -1 - ) { - Log.d("[App Utils] Found a non-emoji character in [$text]") - return false - } - } - } catch (npe: NullPointerException) { - Log.e( - "[App Utils] Can't check emoji presence in text due to NPE in EmojiCompat library, assuming there is none" - ) - // This can happen in EmojiCompat library, mProcessor can be null (https://issuetracker.google.com/issues/277182750) - return false - } - - Log.d("[App Utils] It seems text [$text] only contains emoji(s)") - return true - } - - fun pixelsToDp(pixels: Float): Float { - return TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - pixels, - coreContext.context.resources.displayMetrics - ) - } - - fun dpToPixels(context: Context, dp: Float): Float { - return dp * context.resources.displayMetrics.density - } - - fun bytesToDisplayableSize(bytes: Long): String { - return formatShortFileSize(coreContext.context, bytes) - } - - fun shareUploadedLogsUrl(activity: Activity, info: String) { - val appName = activity.getString(R.string.app_name) - val intent = Intent(Intent.ACTION_SEND) - intent.putExtra( - Intent.EXTRA_EMAIL, - arrayOf(activity.getString(R.string.about_bugreport_email)) - ) - intent.putExtra(Intent.EXTRA_SUBJECT, "$appName Logs") - intent.putExtra(Intent.EXTRA_TEXT, info) - intent.type = "text/plain" - - try { - activity.startActivity( - Intent.createChooser( - intent, - activity.getString(R.string.share_uploaded_logs_link) - ) - ) - } catch (ex: ActivityNotFoundException) { - Log.e(ex) - } - } - - fun getDividerDecoration(context: Context, layoutManager: LinearLayoutManager): DividerItemDecoration { - val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation) - val divider = ResourcesCompat.getDrawable(context.resources, R.drawable.divider, null) - if (divider != null) dividerItemDecoration.setDrawable(divider) - return dividerItemDecoration - } - - fun createBundleWithSharedTextAndFiles(sharedViewModel: SharedMainViewModel): Bundle { - val bundle = Bundle() - bundle.putString("TextToShare", sharedViewModel.textToShare.value.orEmpty()) - bundle.putStringArrayList("FilesToShare", sharedViewModel.filesToShare.value) - - // Remove values from shared view model - sharedViewModel.textToShare.value = "" - sharedViewModel.filesToShare.value = arrayListOf() - - return bundle - } - - fun isMediaVolumeLow(context: Context): Boolean { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) - val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC) - Log.i("[Media Volume] Current value is $currentVolume, max value is $maxVolume") - return currentVolume <= maxVolume * 0.5 - } - - fun acquireAudioFocusForVoiceRecordingOrPlayback(context: Context): AudioFocusRequestCompat { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - val audioAttrs = AudioAttributesCompat.Builder() - .setUsage(AudioAttributesCompat.USAGE_MEDIA) - .setContentType(AudioAttributesCompat.CONTENT_TYPE_SPEECH) - .build() - - val request = - AudioFocusRequestCompat.Builder( - AudioManagerCompat.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE - ) - .setAudioAttributes(audioAttrs) - .setOnAudioFocusChangeListener { } - .build() - when (AudioManagerCompat.requestAudioFocus(audioManager, request)) { - AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> { - Log.i("[Audio Focus] Voice recording/playback audio focus request granted") - } - AudioManager.AUDIOFOCUS_REQUEST_FAILED -> { - Log.w("[Audio Focus] Voice recording/playback audio focus request failed") - } - AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> { - Log.w("[Audio Focus] Voice recording/playback audio focus request delayed") - } - } - return request - } - - fun releaseAudioFocusForVoiceRecordingOrPlayback( - context: Context, - request: AudioFocusRequestCompat - ) { - val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager - AudioManagerCompat.abandonAudioFocusRequest(audioManager, request) - Log.i("[Audio Focus] Voice recording/playback audio focus request abandoned") - } - } -} diff --git a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt b/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt deleted file mode 100644 index fd95886fd..000000000 --- a/app/src/main/java/org/linphone/utils/AudioRouteUtils.kt +++ /dev/null @@ -1,343 +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 . - */ -package org.linphone.utils - -import android.telecom.CallAudioState -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.compatibility.Compatibility -import org.linphone.core.AudioDevice -import org.linphone.core.Call -import org.linphone.core.tools.Log -import org.linphone.telecom.TelecomHelper - -class AudioRouteUtils { - companion object { - private fun applyAudioRouteChange( - call: Call?, - types: List, - output: Boolean = true - ) { - val currentCall = if (coreContext.core.callsNb > 0) { - call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] - } else { - Log.w("[Audio Route Helper] No call found, setting audio route on Core") - null - } - val conference = coreContext.core.conference - val capability = if (output) { - AudioDevice.Capabilities.CapabilityPlay - } else { - AudioDevice.Capabilities.CapabilityRecord - } - val preferredDriver = if (output) { - coreContext.core.defaultOutputAudioDevice?.driverName - } else { - coreContext.core.defaultInputAudioDevice?.driverName - } - - val extendedAudioDevices = coreContext.core.extendedAudioDevices - Log.i( - "[Audio Route Helper] Looking for an ${if (output) "output" else "input"} audio device with capability [$capability], driver name [$preferredDriver] and type [$types] in extended audio devices list (size ${extendedAudioDevices.size})" - ) - val foundAudioDevice = extendedAudioDevices.find { - it.driverName == preferredDriver && types.contains(it.type) && it.hasCapability( - capability - ) - } - val audioDevice = if (foundAudioDevice == null) { - Log.w( - "[Audio Route Helper] Failed to find an audio device with capability [$capability], driver name [$preferredDriver] and type [$types]" - ) - extendedAudioDevices.find { - types.contains(it.type) && it.hasCapability(capability) - } - } else { - foundAudioDevice - } - - if (audioDevice == null) { - Log.e( - "[Audio Route Helper] Couldn't find audio device with capability [$capability] and type [$types]" - ) - for (device in extendedAudioDevices) { - // TODO: switch to debug? - Log.i( - "[Audio Route Helper] Extended audio device: [${device.deviceName} (${device.driverName}) ${device.type} / ${device.capabilities}]" - ) - } - return - } - if (conference != null && conference.isIn) { - Log.i( - "[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], routing conference audio to it" - ) - if (output) { - conference.outputAudioDevice = audioDevice - } else { - conference.inputAudioDevice = audioDevice - } - } else if (currentCall != null) { - Log.i( - "[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], routing call audio to it" - ) - if (output) { - currentCall.outputAudioDevice = audioDevice - } else { - currentCall.inputAudioDevice = audioDevice - } - } else { - Log.i( - "[Audio Route Helper] Found [${audioDevice.type}] ${if (output) "playback" else "recorder"} audio device [${audioDevice.deviceName} (${audioDevice.driverName})], changing core default audio device" - ) - if (output) { - coreContext.core.outputAudioDevice = audioDevice - } else { - coreContext.core.inputAudioDevice = audioDevice - } - } - } - - private fun routeAudioTo( - call: Call?, - types: List, - skipTelecom: Boolean = false - ) { - val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls.firstOrNull() - if (currentCall != null && !skipTelecom && TelecomHelper.exists()) { - Log.i( - "[Audio Route Helper] Call provided & Telecom Helper exists, trying to dispatch audio route change through Telecom API" - ) - val connection = TelecomHelper.get().findConnectionForCallId( - currentCall.callLog.callId.orEmpty() - ) - if (connection != null) { - val route = when (types.first()) { - AudioDevice.Type.Earpiece -> CallAudioState.ROUTE_EARPIECE - AudioDevice.Type.Speaker -> CallAudioState.ROUTE_SPEAKER - AudioDevice.Type.Headphones, AudioDevice.Type.Headset -> CallAudioState.ROUTE_WIRED_HEADSET - AudioDevice.Type.Bluetooth, AudioDevice.Type.BluetoothA2DP, AudioDevice.Type.HearingAid -> CallAudioState.ROUTE_BLUETOOTH - else -> CallAudioState.ROUTE_WIRED_OR_EARPIECE - } - Log.i( - "[Audio Route Helper] Telecom Helper & matching connection found, dispatching audio route change through it" - ) - // We will be called here again by NativeCallWrapper.onCallAudioStateChanged() - // but this time with skipTelecom = true - if (!Compatibility.changeAudioRouteForTelecomManager(connection, route)) { - Log.w( - "[Audio Route Helper] Connection is already using this route internally, make the change!" - ) - applyAudioRouteChange(currentCall, types) - } - } else { - Log.w("[Audio Route Helper] Telecom Helper found but no matching connection!") - applyAudioRouteChange(currentCall, types) - } - } else { - applyAudioRouteChange(call, types) - } - } - - fun routeAudioToEarpiece(call: Call? = null, skipTelecom: Boolean = false) { - routeAudioTo(call, arrayListOf(AudioDevice.Type.Earpiece), skipTelecom) - } - - fun routeAudioToSpeaker(call: Call? = null, skipTelecom: Boolean = false) { - routeAudioTo(call, arrayListOf(AudioDevice.Type.Speaker), skipTelecom) - } - - fun routeAudioToBluetooth(call: Call? = null, skipTelecom: Boolean = false) { - routeAudioTo( - call, - arrayListOf(AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid), - skipTelecom - ) - } - - fun routeAudioToHeadset(call: Call? = null, skipTelecom: Boolean = false) { - routeAudioTo( - call, - arrayListOf(AudioDevice.Type.Headphones, AudioDevice.Type.Headset), - skipTelecom - ) - } - - fun isSpeakerAudioRouteCurrentlyUsed(call: Call? = null): Boolean { - val currentCall = if (coreContext.core.callsNb > 0) { - call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] - } else { - Log.w("[Audio Route Helper] No call found, checking audio route on Core") - null - } - val conference = coreContext.core.conference - - val audioDevice = if (conference != null && conference.isIn) { - conference.outputAudioDevice - } else if (currentCall != null) { - currentCall.outputAudioDevice - } else { - coreContext.core.outputAudioDevice - } - - if (audioDevice == null) return false - Log.i( - "[Audio Route Helper] Playback audio device currently in use is [${audioDevice.deviceName} (${audioDevice.driverName}) ${audioDevice.type}]" - ) - return audioDevice.type == AudioDevice.Type.Speaker - } - - fun isBluetoothAudioRouteCurrentlyUsed(call: Call? = null): Boolean { - if (coreContext.core.callsNb == 0) { - Log.w("[Audio Route Helper] No call found, so bluetooth audio route isn't used") - return false - } - val currentCall = call ?: coreContext.core.currentCall ?: coreContext.core.calls[0] - val conference = coreContext.core.conference - - val audioDevice = if (conference != null && conference.isIn) { - conference.outputAudioDevice - } else { - currentCall.outputAudioDevice - } - - if (audioDevice == null) return false - Log.i( - "[Audio Route Helper] Playback audio device currently in use is [${audioDevice.deviceName} (${audioDevice.driverName}) ${audioDevice.type}]" - ) - return audioDevice.type == AudioDevice.Type.Bluetooth || audioDevice.type == AudioDevice.Type.HearingAid - } - - fun isBluetoothAudioRouteAvailable(): Boolean { - for (audioDevice in coreContext.core.audioDevices) { - if ((audioDevice.type == AudioDevice.Type.Bluetooth || audioDevice.type == AudioDevice.Type.HearingAid) && - audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay) - ) { - Log.i( - "[Audio Route Helper] Found bluetooth audio device [${audioDevice.deviceName} (${audioDevice.driverName})]" - ) - return true - } - } - return false - } - - private fun isBluetoothAudioRecorderAvailable(): Boolean { - for (audioDevice in coreContext.core.audioDevices) { - if ((audioDevice.type == AudioDevice.Type.Bluetooth || audioDevice.type == AudioDevice.Type.HearingAid) && - audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityRecord) - ) { - Log.i( - "[Audio Route Helper] Found bluetooth audio recorder [${audioDevice.deviceName} (${audioDevice.driverName})]" - ) - return true - } - } - return false - } - - fun isHeadsetAudioRouteAvailable(): Boolean { - for (audioDevice in coreContext.core.audioDevices) { - if ((audioDevice.type == AudioDevice.Type.Headset || audioDevice.type == AudioDevice.Type.Headphones) && - audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityPlay) - ) { - Log.i( - "[Audio Route Helper] Found headset/headphones audio device [${audioDevice.deviceName} (${audioDevice.driverName})]" - ) - return true - } - } - return false - } - - private fun isHeadsetAudioRecorderAvailable(): Boolean { - for (audioDevice in coreContext.core.audioDevices) { - if ((audioDevice.type == AudioDevice.Type.Headset || audioDevice.type == AudioDevice.Type.Headphones) && - audioDevice.hasCapability(AudioDevice.Capabilities.CapabilityRecord) - ) { - Log.i( - "[Audio Route Helper] Found headset/headphones audio recorder [${audioDevice.deviceName} (${audioDevice.driverName})]" - ) - return true - } - } - return false - } - - fun getAudioPlaybackDeviceIdForCallRecordingOrVoiceMessage(): String? { - // In case no headphones/headset/hearing aid/bluetooth is connected, use speaker sound card to play recordings, otherwise use earpiece - // If none are available, default one will be used - var headphonesCard: String? = null - var bluetoothCard: String? = null - var speakerCard: String? = null - var earpieceCard: String? = null - for (device in coreContext.core.audioDevices) { - if (device.hasCapability(AudioDevice.Capabilities.CapabilityPlay)) { - when (device.type) { - AudioDevice.Type.Headphones, AudioDevice.Type.Headset -> { - headphonesCard = device.id - } - AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid -> { - bluetoothCard = device.id - } - AudioDevice.Type.Speaker -> { - speakerCard = device.id - } - AudioDevice.Type.Earpiece -> { - earpieceCard = device.id - } - else -> {} - } - } - } - Log.i( - "[Audio Route Helper] Found headset/headphones/hearingAid sound card [$headphonesCard], bluetooth sound card [$bluetoothCard], speaker sound card [$speakerCard] and earpiece sound card [$earpieceCard]" - ) - return headphonesCard ?: bluetoothCard ?: speakerCard ?: earpieceCard - } - - fun getAudioRecordingDeviceForVoiceMessage(): AudioDevice? { - // In case no headphones/headset/hearing aid/bluetooth is connected, use microphone - // If none are available, default one will be used - var bluetoothAudioDevice: AudioDevice? = null - var headsetAudioDevice: AudioDevice? = null - var builtinMicrophone: AudioDevice? = null - for (device in coreContext.core.audioDevices) { - if (device.hasCapability(AudioDevice.Capabilities.CapabilityRecord)) { - when (device.type) { - AudioDevice.Type.Bluetooth, AudioDevice.Type.HearingAid -> { - bluetoothAudioDevice = device - } - AudioDevice.Type.Headset, AudioDevice.Type.Headphones -> { - headsetAudioDevice = device - } - AudioDevice.Type.Microphone -> { - builtinMicrophone = device - } - else -> {} - } - } - } - Log.i( - "[Audio Route Helper] Found headset/headphones/hearingAid [${headsetAudioDevice?.id}], bluetooth [${bluetoothAudioDevice?.id}] and builtin microphone [${builtinMicrophone?.id}]" - ) - return headsetAudioDevice ?: bluetoothAudioDevice ?: builtinMicrophone - } - } -} diff --git a/app/src/main/java/org/linphone/utils/ContactUtils.kt b/app/src/main/java/org/linphone/utils/ContactUtils.kt deleted file mode 100644 index 1c4098aa7..000000000 --- a/app/src/main/java/org/linphone/utils/ContactUtils.kt +++ /dev/null @@ -1,71 +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 . - */ -package org.linphone.utils - -import android.content.ContentResolver -import android.net.Uri -import android.provider.ContactsContract -import java.io.* -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.core.tools.Log - -class ContactUtils { - companion object { - fun getContactVcardFilePath(contactUri: Uri): String? { - val contentResolver: ContentResolver = coreContext.context.contentResolver - val lookupUri = ContactsContract.Contacts.getLookupUri(contentResolver, contactUri) - Log.i("[Contact Utils] Contact lookup URI is $lookupUri") - - val contactID = FileUtils.getNameFromFilePath(lookupUri.toString()) - Log.i("[Contact Utils] Contact ID is $contactID") - - val contact = coreContext.contactsManager.findContactById(contactID) - if (contact == null) { - Log.e("[Contact Utils] Failed to find contact with ID $contactID") - return null - } - - val vcard = contact.vcard?.asVcard4String() - if (vcard == null) { - Log.e("[Contact Utils] Failed to get vCard from contact $contactID") - return null - } - - val contactName = contact.name?.replace(" ", "_") ?: contactID - val vcardPath = FileUtils.getFileStoragePath("$contactName.vcf") - val inputStream = ByteArrayInputStream(vcard.toByteArray()) - try { - FileOutputStream(vcardPath).use { out -> - val buffer = ByteArray(4096) - var bytesRead: Int - while (inputStream.read(buffer).also { bytesRead = it } >= 0) { - out.write(buffer, 0, bytesRead) - } - } - } catch (e: IOException) { - Log.e("[Contact Utils] creating vcard file exception: $e") - return null - } - - Log.i("[Contact Utils] Contact vCard path is $vcardPath") - return vcardPath.absolutePath - } - } -} diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt deleted file mode 100644 index e7bd08092..000000000 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ /dev/null @@ -1,898 +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 . - */ -package org.linphone.utils - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.text.Editable -import android.text.TextWatcher -import android.util.Patterns -import android.view.* -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.widget.* -import android.widget.SeekBar.OnSeekBarChangeListener -import androidx.appcompat.content.res.AppCompatResources -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.Guideline -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.doOnLayout -import androidx.databinding.* -import androidx.emoji2.emojipicker.EmojiPickerView -import androidx.emoji2.emojipicker.EmojiViewItem -import androidx.lifecycle.LifecycleOwner -import coil.dispose -import coil.load -import coil.request.CachePolicy -import coil.request.videoFrameMillis -import coil.transform.CircleCropTransformation -import com.google.android.material.switchmaterial.SwitchMaterial -import kotlinx.coroutines.* -import org.linphone.BR -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.activities.main.settings.SettingListener -import org.linphone.activities.voip.data.ConferenceParticipantDeviceData -import org.linphone.activities.voip.views.ScrollDotsView -import org.linphone.contact.ContactAvatarGenerator -import org.linphone.contact.ContactDataInterface -import org.linphone.contact.getPictureUri -import org.linphone.core.ConsolidatedPresence -import org.linphone.core.tools.Log -import org.linphone.views.VoiceRecordProgressBar - -/** - * This file contains all the data binding necessary for the app - */ - -fun View.hideKeyboard() { - try { - val imm = - context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(windowToken, 0) - } catch (_: Exception) {} -} - -fun View.setKeyboardInsetListener(lambda: (visible: Boolean) -> Unit) { - doOnLayout { - var isKeyboardVisible = ViewCompat.getRootWindowInsets(this)?.isVisible( - WindowInsetsCompat.Type.ime() - ) == true - - lambda(isKeyboardVisible) - - // See https://issuetracker.google.com/issues/281942480 - ViewCompat.setOnApplyWindowInsetsListener( - rootView - ) { view, insets -> - val keyboardVisibilityChanged = ViewCompat.getRootWindowInsets(view) - ?.isVisible(WindowInsetsCompat.Type.ime()) == true - if (keyboardVisibilityChanged != isKeyboardVisible) { - isKeyboardVisible = keyboardVisibilityChanged - lambda(isKeyboardVisible) - } - ViewCompat.onApplyWindowInsets(view, insets) - } - } -} - -@BindingAdapter("android:src") -fun ImageView.setSourceImageResource(resource: Int) { - this.setImageResource(resource) -} - -@BindingAdapter("android:contentDescription") -fun ImageView.setContentDescription(resource: Int) { - if (resource == 0) { - Log.w("Can't set content description with resource id 0") - return - } - this.contentDescription = context.getString(resource) -} - -@BindingAdapter("android:textStyle") -fun TextView.setTypeface(typeface: Int) { - this.setTypeface(null, typeface) -} - -@BindingAdapter("android:layout_size") -fun View.setLayoutSize(dimension: Float) { - if (dimension == 0f) return - this.layoutParams.height = dimension.toInt() - this.layoutParams.width = dimension.toInt() -} - -@BindingAdapter("backgroundImage") -fun LinearLayout.setBackgroundImage(resource: Int) { - this.setBackgroundResource(resource) -} - -@Suppress("DEPRECATION") -@BindingAdapter("style") -fun TextView.setStyle(resource: Int) { - this.setTextAppearance(context, resource) -} - -@BindingAdapter("android:layout_marginLeft") -fun setLeftMargin(view: View, margin: Float) { - val layoutParams = view.layoutParams as RelativeLayout.LayoutParams - layoutParams.leftMargin = margin.toInt() - view.layoutParams = layoutParams -} - -@BindingAdapter("android:layout_marginRight") -fun setRightMargin(view: View, margin: Float) { - val layoutParams = view.layoutParams as RelativeLayout.LayoutParams - layoutParams.rightMargin = margin.toInt() - view.layoutParams = layoutParams -} - -@BindingAdapter("android:layout_alignLeft") -fun setLayoutLeftAlign(view: View, oldTargetId: Int, newTargetId: Int) { - val layoutParams = view.layoutParams as RelativeLayout.LayoutParams - if (oldTargetId != 0) layoutParams.removeRule(RelativeLayout.ALIGN_LEFT) - if (newTargetId != 0) layoutParams.addRule(RelativeLayout.ALIGN_LEFT, newTargetId) - view.layoutParams = layoutParams -} - -@BindingAdapter("android:layout_alignRight") -fun setLayoutRightAlign(view: View, oldTargetId: Int, newTargetId: Int) { - val layoutParams = view.layoutParams as RelativeLayout.LayoutParams - if (oldTargetId != 0) layoutParams.removeRule(RelativeLayout.ALIGN_RIGHT) - if (newTargetId != 0) layoutParams.addRule(RelativeLayout.ALIGN_RIGHT, newTargetId) - view.layoutParams = layoutParams -} - -@BindingAdapter("android:layout_toLeftOf") -fun setLayoutToLeftOf(view: View, oldTargetId: Int, newTargetId: Int) { - val layoutParams = view.layoutParams as RelativeLayout.LayoutParams - if (oldTargetId != 0) layoutParams.removeRule(RelativeLayout.LEFT_OF) - if (newTargetId != 0) layoutParams.addRule(RelativeLayout.LEFT_OF, newTargetId) - view.layoutParams = layoutParams -} - -@BindingAdapter("android:layout_gravity") -fun setLayoutGravity(view: View, gravity: Int) { - val layoutParams = view.layoutParams as LinearLayout.LayoutParams - layoutParams.gravity = gravity - view.layoutParams = layoutParams -} - -@BindingAdapter("layout_constraintGuide_percent") -fun setLayoutConstraintGuidePercent(guideline: Guideline, percent: Float) { - val params = guideline.layoutParams as ConstraintLayout.LayoutParams - params.guidePercent = percent - guideline.layoutParams = params -} - -@BindingAdapter("onClickToggleSwitch") -fun switchSetting(view: View, switchId: Int) { - val switch: SwitchMaterial = view.findViewById(switchId) - view.setOnClickListener { switch.isChecked = !switch.isChecked } -} - -@BindingAdapter("onValueChanged") -fun editTextSetting(view: EditText, lambda: () -> Unit) { - view.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - lambda() - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - }) -} - -@BindingAdapter("onSettingImeDone") -fun editTextImeDone(view: EditText, lambda: () -> Unit) { - view.setOnEditorActionListener { _, actionId, _ -> - if (actionId == EditorInfo.IME_ACTION_DONE) { - lambda() - - view.clearFocus() - - val imm = view.context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(view.windowToken, 0) - - return@setOnEditorActionListener true - } - false - } -} - -@BindingAdapter("onFocusChangeVisibilityOf") -fun setEditTextOnFocusChangeVisibilityOf(editText: EditText, view: View) { - editText.setOnFocusChangeListener { _, hasFocus -> - view.visibility = if (hasFocus) View.VISIBLE else View.INVISIBLE - } -} - -@BindingAdapter("selectedIndex", "settingListener") -fun spinnerSetting(spinner: Spinner, selectedIndex: Int, listener: SettingListener?) { - spinner.setSelection(selectedIndex, true) - - spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onNothingSelected(parent: AdapterView<*>?) {} - - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - // From Crashlytics it seems this method may be called with a null listener - listener?.onListValueChanged(position) - } - } -} - -@BindingAdapter("onProgressChanged") -fun setListener(view: SeekBar, lambda: (Any) -> Unit) { - view.setOnSeekBarChangeListener(object : OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - if (fromUser) lambda(progress) - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) {} - - override fun onStopTrackingTouch(seekBar: SeekBar?) {} - }) -} - -@BindingAdapter("inflatedLifecycleOwner") -fun setInflatedViewStubLifecycleOwner(view: View, enable: Boolean) { - val binding = DataBindingUtil.bind(view) - // This is a bit hacky... - if (view.context is LifecycleOwner) { - binding?.lifecycleOwner = view.context as? LifecycleOwner - } -} - -@BindingAdapter("entries") -fun setEntries( - viewGroup: ViewGroup, - entries: List? -) { - viewGroup.removeAllViews() - if (entries != null) { - for (i in entries) { - viewGroup.addView(i.root) - } - } -} - -private fun setEntries( - viewGroup: ViewGroup, - entries: List?, - layoutId: Int, - onLongClick: View.OnLongClickListener?, - parent: Any? -) { - viewGroup.removeAllViews() - if (entries != null) { - val inflater = viewGroup.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - for (entry in entries) { - val binding = DataBindingUtil.inflate( - inflater, - layoutId, - viewGroup, - false - ) - binding.setVariable(BR.data, entry) - binding.setVariable(BR.longClickListener, onLongClick) - binding.setVariable(BR.parent, parent) - - // This is a bit hacky... - if (viewGroup.context is LifecycleOwner) { - binding.lifecycleOwner = viewGroup.context as? LifecycleOwner - } - - viewGroup.addView(binding.root) - } - } -} - -@BindingAdapter("entries", "layout") -fun setEntries( - viewGroup: ViewGroup, - entries: List?, - layoutId: Int -) { - setEntries(viewGroup, entries, layoutId, null, null) -} - -@BindingAdapter("entries", "layout", "onLongClick") -fun setEntries( - viewGroup: ViewGroup, - entries: List?, - layoutId: Int, - onLongClick: View.OnLongClickListener? -) { - setEntries(viewGroup, entries, layoutId, onLongClick, null) -} - -@BindingAdapter("entries", "layout", "parent") -fun setEntries( - viewGroup: ViewGroup, - entries: List?, - layoutId: Int, - parent: Any? -) { - setEntries(viewGroup, entries, layoutId, null, parent) -} - -@BindingAdapter("android:scaleType") -fun setImageViewScaleType(imageView: ImageView, scaleType: ImageView.ScaleType) { - imageView.scaleType = scaleType -} - -@BindingAdapter("coilRounded") -fun loadRoundImageWithCoil(imageView: ImageView, path: String?) { - if (!path.isNullOrEmpty() && FileUtils.isExtensionImage(path)) { - imageView.load(path) { - transformations(CircleCropTransformation()) - } - } else { - Log.w("[Data Binding] [Coil] Can't load $path") - } -} - -@BindingAdapter("coil") -fun loadImageWithCoil(imageView: ImageView, path: String?) { - if (!path.isNullOrEmpty() && FileUtils.isExtensionImage(path)) { - if (corePreferences.vfsEnabled && path.startsWith(corePreferences.vfsCachePath)) { - imageView.load(path) { - diskCachePolicy(CachePolicy.DISABLED) - listener( - onError = { _, result -> - Log.e( - "[Data Binding] [VFS] [Coil] Error loading [$path]: ${result.throwable}" - ) - } - ) - } - } else { - imageView.load(path) { - listener( - onError = { _, result -> - Log.e("[Data Binding] [Coil] Error loading [$path]: ${result.throwable}") - } - ) - } - } - } else if (path != null) { - Log.w("[Data Binding] [Coil] Can't load $path") - } -} - -private suspend fun loadContactPictureWithCoil( - imageView: ImageView, - contact: ContactDataInterface?, - useThumbnail: Boolean, - size: Int = 0, - textSize: Int = 0, - color: Int = 0, - textColor: Int = 0, - defaultAvatar: String? = null -) { - imageView.dispose() - - val context = imageView.context - if (contact == null) { - if (defaultAvatar != null) { - imageView.load(defaultAvatar) { - transformations(CircleCropTransformation()) - } - } else { - imageView.load(R.drawable.icon_single_contact_avatar) - } - } else if (contact.showGroupChatAvatar) { - imageView.load( - AppCompatResources.getDrawable(context, R.drawable.icon_multiple_contacts_avatar) - ) - } else { - val displayName = contact.contact.value?.name ?: contact.displayName.value.orEmpty() - val source = contact.contact.value?.getPictureUri(useThumbnail) - val sourceStr = source.toString() - val base64 = if (ImageUtils.isBase64(sourceStr)) { - Log.d("[Coil] Picture URI is base64 encoded") - ImageUtils.getBase64ImageFromString(sourceStr) - } else { - null - } - - imageView.load(base64 ?: source) { - transformations(CircleCropTransformation()) - error( - if (displayName.isEmpty() || AppUtils.getInitials(displayName) == "+") { - AppCompatResources.getDrawable(context, R.drawable.icon_single_contact_avatar) - } else { - coroutineScope { - withContext(Dispatchers.IO) { - val builder = ContactAvatarGenerator(context) - builder.setLabel(displayName) - if (size > 0) { - builder.setAvatarSize(AppUtils.getDimension(size).toInt()) - } - if (textSize > 0) { - builder.setTextSize(AppUtils.getDimension(textSize)) - } - if (color > 0) { - builder.setBackgroundColorAttribute(color) - } - if (textColor > 0) { - builder.setTextColorResource(textColor) - } - builder.build() - } - } - } - ) - } - } -} - -@BindingAdapter("coilContact") -fun loadContactPictureWithCoil(imageView: ImageView, contact: ContactDataInterface?) { - val coroutineScope = contact?.coroutineScope ?: coreContext.coroutineScope - coroutineScope.launch { - withContext(Dispatchers.Main) { - loadContactPictureWithCoil(imageView, contact, true) - } - } -} - -@BindingAdapter("coilContactBig") -fun loadBigContactPictureWithCoil(imageView: ImageView, contact: ContactDataInterface?) { - val coroutineScope = contact?.coroutineScope ?: coreContext.coroutineScope - coroutineScope.launch { - withContext(Dispatchers.Main) { - loadContactPictureWithCoil( - imageView, - contact, - false, - R.dimen.contact_avatar_big_size, - R.dimen.contact_avatar_text_big_size - ) - } - } -} - -@BindingAdapter("coilVoipContactAlt") -fun loadVoipContactPictureWithCoilAlt(imageView: ImageView, contact: ContactDataInterface?) { - val coroutineScope = contact?.coroutineScope ?: coreContext.coroutineScope - coroutineScope.launch { - withContext(Dispatchers.Main) { - loadContactPictureWithCoil( - imageView, - contact, - false, - R.dimen.voip_contact_avatar_max_size, - R.dimen.voip_contact_avatar_text_size, - R.attr.voipParticipantBackgroundColor, - R.color.white_color - ) - } - } -} - -@BindingAdapter("coilVoipContact") -fun loadVoipContactPictureWithCoil(imageView: ImageView, contact: ContactDataInterface?) { - val coroutineScope = contact?.coroutineScope ?: coreContext.coroutineScope - coroutineScope.launch { - withContext(Dispatchers.Main) { - loadContactPictureWithCoil( - imageView, - contact, - false, - R.dimen.voip_contact_avatar_max_size, - R.dimen.voip_contact_avatar_text_size, - R.attr.voipBackgroundColor, - R.color.white_color - ) - } - } -} - -@BindingAdapter("coilSelfAvatar") -fun loadSelfAvatarWithCoil(imageView: ImageView, contact: ContactDataInterface?) { - val coroutineScope = contact?.coroutineScope ?: coreContext.coroutineScope - coroutineScope.launch { - withContext(Dispatchers.Main) { - loadContactPictureWithCoil( - imageView, - contact, - false, - R.dimen.voip_contact_avatar_max_size, - R.dimen.voip_contact_avatar_text_size, - R.attr.voipBackgroundColor, - R.color.white_color, - corePreferences.defaultAccountAvatarPath - ) - } - } -} - -@BindingAdapter("coilGoneIfError") -fun loadAvatarWithCoil(imageView: ImageView, path: String?) { - if (path != null) { - imageView.visibility = View.VISIBLE - imageView.load(path) { - transformations(CircleCropTransformation()) - listener( - onError = { _, result -> - Log.e("[Data Binding] [Coil] Error loading [$path]: ${result.throwable}") - imageView.visibility = View.GONE - }, - onSuccess = { _, _ -> - imageView.visibility = View.VISIBLE - } - ) - } - } else { - imageView.visibility = View.GONE - } -} - -@BindingAdapter("coilVideoPreview") -fun loadVideoPreview(imageView: ImageView, path: String?) { - if (!path.isNullOrEmpty() && FileUtils.isExtensionVideo(path)) { - imageView.load(path) { - videoFrameMillis(0) - listener( - onError = { _, result -> - Log.e( - "[Data Binding] [Coil] Error getting preview picture from video? [$path]: ${result.throwable}" - ) - }, - onSuccess = { _, _ -> - // Display "play" button above video preview - LayoutInflater.from(imageView.context).inflate( - R.layout.video_play_button, - imageView.parent as ViewGroup - ) - } - ) - } - } -} - -@BindingAdapter("assistantPhoneNumberValidation") -fun addPhoneNumberEditTextValidation(editText: EditText, enabled: Boolean) { - if (!enabled) return - editText.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - when { - s?.matches(Regex("\\d+")) == false -> - editText.error = - editText.context.getString( - R.string.assistant_error_phone_number_invalid_characters - ) - } - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - }) -} - -@BindingAdapter("assistantPhoneNumberPrefixValidation") -fun addPrefixEditTextValidation(editText: EditText, enabled: Boolean) { - if (!enabled) return - editText.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - if ((s?.length ?: 0) > 1) { - val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix( - s.toString().substring(1) - ) - if (dialPlan == null) { - editText.error = - editText.context.getString( - R.string.assistant_error_invalid_international_prefix - ) - } - } - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - @SuppressLint("SetTextI18n") - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (s.isNullOrEmpty() || !s.startsWith("+")) { - editText.setText("+$s") - } - } - }) -} - -@BindingAdapter("assistantUsernameValidation") -fun addUsernameEditTextValidation(editText: EditText, enabled: Boolean) { - if (!enabled) return - val usernameRegexp = corePreferences.config.getString( - "assistant", - "username_regex", - "^[a-z0-9+_.\\-]*\$" - )!! - val usernameMaxLength = corePreferences.config.getInt("assistant", "username_max_length", 64) - editText.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - when { - s?.matches(Regex(usernameRegexp)) == false -> - editText.error = - editText.context.getString( - R.string.assistant_error_username_invalid_characters - ) - (s?.length ?: 0) > usernameMaxLength -> { - editText.error = - editText.context.getString(R.string.assistant_error_username_too_long) - } - } - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - }) -} - -@BindingAdapter("emailConfirmationValidation") -fun addEmailEditTextValidation(editText: EditText, enabled: Boolean) { - if (!enabled) return - editText.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) {} - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (!Patterns.EMAIL_ADDRESS.matcher(s).matches()) { - editText.error = - editText.context.getString(R.string.assistant_error_invalid_email_address) - } - } - }) -} - -@BindingAdapter("urlConfirmationValidation") -fun addUrlEditTextValidation(editText: EditText, enabled: Boolean) { - if (!enabled) return - editText.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) {} - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { - if (!Patterns.WEB_URL.matcher(s).matches()) { - editText.error = - editText.context.getString(R.string.assistant_remote_provisioning_wrong_format) - } - } - }) -} - -@BindingAdapter("passwordConfirmationValidation") -fun addPasswordConfirmationEditTextValidation(password: EditText, passwordConfirmation: EditText) { - password.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) {} - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (passwordConfirmation.text == null || s == null || passwordConfirmation.text.toString() != s.toString()) { - passwordConfirmation.error = - passwordConfirmation.context.getString( - R.string.assistant_error_passwords_dont_match - ) - } else { - passwordConfirmation.error = null // To clear other edit text field error - } - } - }) - - passwordConfirmation.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) {} - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - if (password.text == null || s == null || password.text.toString() != s.toString()) { - passwordConfirmation.error = - passwordConfirmation.context.getString( - R.string.assistant_error_passwords_dont_match - ) - } - } - }) -} - -@BindingAdapter("errorMessage") -fun setEditTextError(editText: EditText, error: String?) { - if (error != editText.error) { - editText.error = error - } -} - -@InverseBindingAdapter(attribute = "errorMessage") -fun getEditTextError(editText: EditText): String? { - return editText.error?.toString() -} - -@BindingAdapter("errorMessageAttrChanged") -fun setEditTextErrorListener(editText: EditText, attrChange: InverseBindingListener) { - editText.addTextChangedListener(object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - attrChange.onChange() - } - - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { - editText.error = null - attrChange.onChange() - } - - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} - }) -} - -@BindingAdapter("max") -fun VoiceRecordProgressBar.setProgressMax(max: Int) { - setMax(max) -} - -@BindingAdapter("android:progress") -fun VoiceRecordProgressBar.setPrimaryProgress(progress: Int) { - setProgress(progress) -} - -@BindingAdapter("android:secondaryProgress") -fun VoiceRecordProgressBar.setSecProgress(progress: Int) { - setSecondaryProgress(progress) -} - -@BindingAdapter("secondaryProgressTint") -fun VoiceRecordProgressBar.setSecProgressTint(color: Int) { - setSecondaryProgressTint(color) -} - -@BindingAdapter("android:layout_margin") -fun setConstraintLayoutMargins(view: View, margins: Float) { - val params = view.layoutParams as ConstraintLayout.LayoutParams - val m = margins.toInt() - params.setMargins(m, m, m, m) - view.layoutParams = params -} - -@BindingAdapter("android:layout_marginTop") -fun setConstraintLayoutTopMargin(view: View, margins: Float) { - val params = view.layoutParams as ConstraintLayout.LayoutParams - val m = margins.toInt() - params.setMargins(params.leftMargin, m, params.rightMargin, params.bottomMargin) - view.layoutParams = params -} - -@BindingAdapter("android:layout_marginBottom") -fun setConstraintLayoutBottomMargin(view: View, margins: Float) { - val params = view.layoutParams as ConstraintLayout.LayoutParams - val m = margins.toInt() - params.setMargins(params.leftMargin, params.topMargin, params.rightMargin, m) - view.layoutParams = params -} - -@BindingAdapter("android:layout_marginEnd") -fun setConstraintLayoutEndMargin(view: View, margins: Float) { - val params = view.layoutParams as ConstraintLayout.LayoutParams - val m = margins.toInt() - params.marginEnd = m - view.layoutParams = params -} - -@BindingAdapter("android:onTouch") -fun View.setTouchListener(listener: View.OnTouchListener?) { - if (listener != null) { - setOnTouchListener(listener) - } -} - -@BindingAdapter("entries") -fun Spinner.setEntries(entries: List?) { - if (entries != null) { - val arrayAdapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, entries) - arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - adapter = arrayAdapter - } -} - -@BindingAdapter("selectedValueAttrChanged") -fun Spinner.setInverseBindingListener(listener: InverseBindingListener) { - onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) { - if (tag != position) { - listener.onChange() - } - } - - override fun onNothingSelected(parent: AdapterView<*>) {} - } -} - -@BindingAdapter("selectedValue") -fun Spinner.setSelectedValue(value: Any?) { - if (adapter != null) { - val position = (adapter as ArrayAdapter).getPosition(value) - setSelection(position, false) - tag = position - } -} - -@InverseBindingAdapter(attribute = "selectedValue", event = "selectedValueAttrChanged") -fun Spinner.getSelectedValue(): Any? { - return selectedItem -} - -@BindingAdapter("participantTextureView") -fun setParticipantTextureView( - textureView: TextureView, - conferenceParticipantData: ConferenceParticipantDeviceData -) { - conferenceParticipantData.setTextureView(textureView) -} - -@BindingAdapter("itemCount") -fun ScrollDotsView.setItems(count: Int) { - setItemCount(count) -} - -@BindingAdapter("selectedDot") -fun ScrollDotsView.setSelectedIndex(index: Int) { - setSelectedDot(index) -} - -@BindingAdapter("presenceIcon") -fun ImageView.setPresenceIcon(presence: ConsolidatedPresence?) { - if (presence == null) return - - val icon = when (presence) { - ConsolidatedPresence.Online -> R.drawable.led_online - ConsolidatedPresence.DoNotDisturb -> R.drawable.led_do_not_disturb - ConsolidatedPresence.Busy -> R.drawable.led_away - else -> R.drawable.led_not_registered - } - setImageResource(icon) - - val contentDescription = when (presence) { - ConsolidatedPresence.Online -> AppUtils.getString( - R.string.content_description_presence_online - ) - ConsolidatedPresence.DoNotDisturb -> AppUtils.getString( - R.string.content_description_presence_do_not_disturb - ) - else -> AppUtils.getString(R.string.content_description_presence_offline) - } - setContentDescription(contentDescription) -} - -interface EmojiPickedListener { - fun onEmojiPicked(item: EmojiViewItem) -} - -@BindingAdapter("emojiPickedListener") -fun EmojiPickerView.setEmojiPickedListener(listener: EmojiPickedListener) { - setOnEmojiPickedListener { emoji -> - listener.onEmojiPicked(emoji) - } -} diff --git a/app/src/main/java/org/linphone/utils/DialogUtils.kt b/app/src/main/java/org/linphone/utils/DialogUtils.kt deleted file mode 100644 index f88a958c1..000000000 --- a/app/src/main/java/org/linphone/utils/DialogUtils.kt +++ /dev/null @@ -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 . - */ -package org.linphone.utils - -import android.app.Dialog -import android.content.Context -import android.graphics.drawable.ColorDrawable -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.Window -import android.view.WindowManager -import androidx.core.content.ContextCompat -import androidx.databinding.DataBindingUtil -import org.linphone.R -import org.linphone.activities.main.viewmodels.DialogViewModel -import org.linphone.databinding.DialogBinding -import org.linphone.databinding.VoipDialogBinding - -class DialogUtils { - companion object { - fun getDialog(context: Context, viewModel: DialogViewModel): Dialog { - val dialog = Dialog(context) - dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) - - val binding: DialogBinding = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.dialog, - null, - false - ) - binding.viewModel = viewModel - dialog.setContentView(binding.root) - - val d: Drawable = ColorDrawable( - ContextCompat.getColor(dialog.context, R.color.dark_grey_color) - ) - d.alpha = 200 - dialog.window - ?.setLayout( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT - ) - dialog.window?.setBackgroundDrawable(d) - return dialog - } - - fun getVoipDialog(context: Context, viewModel: DialogViewModel): Dialog { - val dialog = Dialog(context, R.style.AppTheme) - dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) - - val binding: VoipDialogBinding = DataBindingUtil.inflate( - LayoutInflater.from(context), - R.layout.voip_dialog, - null, - false - ) - binding.viewModel = viewModel - dialog.setContentView(binding.root) - - val d: Drawable = ColorDrawable( - ContextCompat.getColor(dialog.context, R.color.voip_dark_gray) - ) - d.alpha = 166 - dialog.window - ?.setLayout( - WindowManager.LayoutParams.MATCH_PARENT, - WindowManager.LayoutParams.MATCH_PARENT - ) - dialog.window?.setBackgroundDrawable(d) - return dialog - } - } -} diff --git a/app/src/main/java/org/linphone/utils/Event.kt b/app/src/main/java/org/linphone/utils/Event.kt deleted file mode 100644 index 0e3a53f19..000000000 --- a/app/src/main/java/org/linphone/utils/Event.kt +++ /dev/null @@ -1,41 +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 . - */ -package org.linphone.utils - -import java.util.concurrent.atomic.AtomicBoolean - -/** - * This class allows to limit the number of notification for an event. - * The first one to consume the event will stop the dispatch. - */ -open class Event(private val content: T) { - private val handled = AtomicBoolean(false) - - fun consumed(): Boolean { - return handled.get() - } - - fun consume(handleContent: (T) -> Unit) { - if (!handled.get()) { - handled.set(true) - handleContent(content) - } - } -} diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt deleted file mode 100644 index 7300364c1..000000000 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ /dev/null @@ -1,562 +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 . - */ -package org.linphone.utils - -import android.app.Activity -import android.content.ActivityNotFoundException -import android.content.Context -import android.content.Intent -import android.database.CursorIndexOutOfBoundsException -import android.net.Uri -import android.os.Environment -import android.os.ParcelFileDescriptor -import android.os.Process.myUid -import android.provider.OpenableColumns -import android.system.Os.fstat -import android.webkit.MimeTypeMap -import androidx.core.content.FileProvider -import java.io.* -import java.text.SimpleDateFormat -import java.util.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.withContext -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.core.tools.Log - -class FileUtils { - enum class MimeType { - PlainText, - Pdf, - Image, - Video, - Audio, - Unknown - } - - companion object { - fun getNameFromFilePath(filePath: String): String { - var name = filePath - val i = filePath.lastIndexOf('/') - if (i > 0) { - name = filePath.substring(i + 1) - } - return name - } - - fun getExtensionFromFileName(fileName: String): String { - var extension = MimeTypeMap.getFileExtensionFromUrl(fileName) - if (extension.isNullOrEmpty()) { - val i = fileName.lastIndexOf('.') - if (i > 0) { - extension = fileName.substring(i + 1) - } - } - - return extension.lowercase(Locale.getDefault()) - } - - fun getMimeType(type: String?): MimeType { - if (type.isNullOrEmpty()) return MimeType.Unknown - return when { - type.startsWith("image/") -> MimeType.Image - type.startsWith("text/plain") -> MimeType.PlainText - type.startsWith("video/") -> MimeType.Video - type.startsWith("audio/") -> MimeType.Audio - type.startsWith("application/pdf") -> MimeType.Pdf - else -> MimeType.Unknown - } - } - - fun isExtensionImage(path: String): Boolean { - val extension = getExtensionFromFileName(path) - val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - return getMimeType(type) == MimeType.Image - } - - fun isExtensionVideo(path: String): Boolean { - val extension = getExtensionFromFileName(path) - val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - return getMimeType(type) == MimeType.Video - } - - fun clearExistingPlainFiles() { - val dir = File(corePreferences.vfsCachePath) - if (dir.exists()) { - for (file in dir.listFiles().orEmpty()) { - Log.w( - "[File Utils] [VFS] Found forgotten plain file [${file.path}], deleting it" - ) - deleteFile(file.path) - } - } - } - - fun getFileStorageDir(isPicture: Boolean = false): File { - var path: File? = null - if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) { - Log.w("[File Utils] External storage is mounted") - var directory = Environment.DIRECTORY_DOWNLOADS - if (isPicture) { - Log.w("[File Utils] Using pictures directory instead of downloads") - directory = Environment.DIRECTORY_PICTURES - } - path = coreContext.context.getExternalFilesDir(directory) - } - - val returnPath: File = path ?: coreContext.context.filesDir - if (path == null) { - Log.w( - "[File Utils] Couldn't get external storage path, using internal" - ) - } - - return returnPath - } - - private fun getFileStorageCacheDir(fileName: String): File { - val path = coreContext.context.cacheDir - Log.i("[File Utils] Cache directory is: $path") - - var file = File(path, fileName) - var prefix = 1 - while (file.exists()) { - file = File(path, prefix.toString() + "_" + fileName) - Log.w("[File Utils] File with that name already exists, renamed to ${file.name}") - prefix += 1 - } - return file - } - - fun getFileStoragePath(fileName: String): File { - val path = getFileStorageDir(isExtensionImage(fileName)) - var file = File(path, fileName) - - var prefix = 1 - while (file.exists()) { - file = File(path, prefix.toString() + "_" + fileName) - Log.w("[File Utils] File with that name already exists, renamed to ${file.name}") - prefix += 1 - } - return file - } - - suspend fun getFilesPathFromPickerIntent(data: Intent?, temporaryImageFilePath: File?): List { - var filePath: String? = null - if (data != null) { - val clipData = data.clipData - if (clipData != null && clipData.itemCount > 1) { // Multiple selection - Log.i("[File Utils] Found ${clipData.itemCount} elements") - val list = arrayListOf() - for (i in 0 until clipData.itemCount) { - val dataUri = clipData.getItemAt(i).uri - if (dataUri != null) { - filePath = dataUri.toString() - Log.i("[File Utils] Using data URI $filePath") - } - filePath = copyToLocalStorage(filePath) - if (filePath != null) list.add(filePath) - } - return list - } else { // Single selection - val dataUri = if (clipData != null && clipData.itemCount == 1) { - clipData.getItemAt(0).uri - } else { - data.data - } - if (dataUri != null) { - filePath = dataUri.toString() - Log.i("[File Utils] Using data URI $filePath") - } else if (temporaryImageFilePath?.exists() == true) { - filePath = temporaryImageFilePath.absolutePath - Log.i("[File Utils] Data URI is null, using $filePath") - } - filePath = copyToLocalStorage(filePath) - if (filePath != null) return arrayListOf(filePath) - } - } else if (temporaryImageFilePath?.exists() == true) { - filePath = temporaryImageFilePath.absolutePath - Log.i("[File Utils] Data is null, using $filePath") - filePath = copyToLocalStorage(filePath) - if (filePath != null) return arrayListOf(filePath) - } - return arrayListOf() - } - - suspend fun getFilePathFromPickerIntent(data: Intent?, temporaryImageFilePath: File?): String? { - var filePath: String? = null - if (data != null) { - val clipData = data.clipData - if (clipData != null && clipData.itemCount > 1) { // Multiple selection - Log.e("[File Utils] Expecting only one file, got ${clipData.itemCount}") - } else { // Single selection - val dataUri = if (clipData != null && clipData.itemCount == 1) { - clipData.getItemAt(0).uri - } else { - data.data - } - if (dataUri != null) { - filePath = dataUri.toString() - Log.i("[File Utils] Using data URI $filePath") - } else if (temporaryImageFilePath?.exists() == true) { - filePath = temporaryImageFilePath.absolutePath - Log.i("[File Utils] Data URI is null, using $filePath") - } - } - } else if (temporaryImageFilePath?.exists() == true) { - filePath = temporaryImageFilePath.absolutePath - Log.i("[File Utils] Data is null, using $filePath") - } - return copyToLocalStorage(filePath) - } - - suspend fun copyToLocalStorage(filePath: String?): String? { - if (filePath != null) { - val uriToParse = Uri.parse(filePath) - if (filePath.startsWith("content://com.android.contacts/contacts/lookup/")) { - Log.i("[File Utils] Contact sharing URI detected") - return ContactUtils.getContactVcardFilePath(uriToParse) - } else if (filePath.startsWith("content://") || - filePath.startsWith("file://") - ) { - val result = getFilePath(coreContext.context, uriToParse) - Log.i( - "[File Utils] Path was using a content or file scheme, real path is: $result" - ) - if (result == null) { - Log.e("[File Utils] Failed to get access to file $uriToParse") - } - return result - } - } - return filePath - } - - fun deleteFile(filePath: String) { - val file = File(filePath) - if (file.exists()) { - try { - if (file.delete()) { - Log.i("[File Utils] Deleted $filePath") - } else { - Log.e("[File Utils] Can't delete $filePath") - } - } catch (e: Exception) { - Log.e("[File Utils] Can't delete $filePath, exception: $e") - } - } else { - Log.e("[File Utils] File $filePath doesn't exists") - } - } - - suspend fun getFilePath(context: Context, uri: Uri): String? { - var result: String? = null - val name: String = getNameFromUri(uri, context) - - try { - if (fstat( - ParcelFileDescriptor.open( - File(uri.path), - ParcelFileDescriptor.MODE_READ_ONLY - ).fileDescriptor - ).st_uid != myUid() - ) { - Log.e("[File Utils] File descriptor UID different from our, denying copy!") - return result - } - } catch (e: Exception) { - Log.e("[File Utils] Can't check file ownership: ", e) - } - - try { - val localFile: File = createFile(name) - val remoteFile = - context.contentResolver.openInputStream(uri) - Log.i( - "[File Utils] Trying to copy file from " + - uri.toString() + - " to local file " + - localFile.absolutePath - ) - coroutineScope { - val deferred = async { copyToFile(remoteFile, localFile) } - if (deferred.await()) { - Log.i("[File Utils] Copy successful") - result = localFile.absolutePath - } else { - Log.e("[File Utils] Copy failed") - } - withContext(Dispatchers.IO) { - remoteFile?.close() - } - } - } catch (e: IOException) { - Log.e("[File Utils] getFilePath exception: ", e) - } - - return result - } - - private fun getNameFromUri(uri: Uri, context: Context): String { - var name = "" - if (uri.scheme == "content") { - val returnCursor = - context.contentResolver.query(uri, null, null, null, null) - if (returnCursor != null) { - returnCursor.moveToFirst() - val nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - if (nameIndex != -1) { - try { - val displayName = returnCursor.getString(nameIndex) - if (displayName != null) { - name = displayName - } else { - Log.e( - "[File Utils] Failed to get the display name for URI $uri, returned value is null" - ) - } - } catch (e: CursorIndexOutOfBoundsException) { - Log.e( - "[File Utils] Failed to get the display name for URI $uri, exception is $e" - ) - } - } else { - Log.e("[File Utils] Couldn't get DISPLAY_NAME column index for URI: $uri") - } - returnCursor.close() - } - } else if (uri.scheme == "file") { - name = uri.lastPathSegment ?: "" - } - return name - } - - suspend fun copyFileTo(filePath: String, outputStream: OutputStream?): Boolean { - if (outputStream == null) { - Log.e("[File Utils] Can't copy file $filePath to given null output stream") - return false - } - - val file = File(filePath) - if (!file.exists()) { - Log.e("[File Utils] Can't copy file $filePath, it doesn't exists") - return false - } - - try { - withContext(Dispatchers.IO) { - val inputStream = FileInputStream(file) - val buffer = ByteArray(4096) - var bytesRead: Int - while (inputStream.read(buffer).also { bytesRead = it } >= 0) { - outputStream.write(buffer, 0, bytesRead) - } - } - return true - } catch (e: IOException) { - Log.e("[File Utils] copyFileTo exception: $e") - } - return false - } - - private suspend fun copyToFile(inputStream: InputStream?, destFile: File?): Boolean { - if (inputStream == null || destFile == null) return false - try { - withContext(Dispatchers.IO) { - FileOutputStream(destFile).use { out -> - val buffer = ByteArray(4096) - var bytesRead: Int - while (inputStream.read(buffer).also { bytesRead = it } >= 0) { - out.write(buffer, 0, bytesRead) - } - } - } - return true - } catch (e: IOException) { - Log.e("[File Utils] copyToFile exception: $e") - } - return false - } - - suspend fun copyFileToCache(plainFilePath: String): String? { - val cacheFile = getFileStorageCacheDir(getNameFromFilePath(plainFilePath)) - try { - withContext(Dispatchers.IO) { - FileOutputStream(cacheFile).use { out -> - copyFileTo(plainFilePath, out) - } - } - return cacheFile.absolutePath - } catch (e: IOException) { - Log.e("[File Utils] copyFileToCache exception: $e") - } - return null - } - - private fun createFile(file: String): File { - var fileName = file - - if (fileName.isEmpty()) fileName = getStartDate() - if (!fileName.contains(".")) { - fileName = "$fileName.unknown" - } - - return getFileStoragePath(fileName) - } - - private fun getStartDate(): String { - return try { - SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ROOT).format(Date()) - } catch (e: RuntimeException) { - SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) - } - } - - fun getPublicFilePath(context: Context, path: String): Uri { - val contentUri: Uri - when { - path.startsWith("file://") -> { - val file = File(path.substring("file://".length)) - contentUri = FileProvider.getUriForFile( - context, - context.getString(R.string.file_provider), - file - ) - } - path.startsWith("content://") -> { - contentUri = Uri.parse(path) - } - else -> { - val file = File(path) - contentUri = try { - FileProvider.getUriForFile( - context, - context.getString(R.string.file_provider), - file - ) - } catch (e: Exception) { - Log.e( - "[Chat Message] Couldn't get URI for file $file using file provider ${context.getString( - R.string.file_provider - )}" - ) - Uri.parse(path) - } - } - } - return contentUri - } - - fun openFileInThirdPartyApp( - activity: Activity, - contentFilePath: String, - newTask: Boolean = false - ): Boolean { - val intent = Intent(Intent.ACTION_VIEW) - val contentUri: Uri = getPublicFilePath(activity, contentFilePath) - val filePath: String = contentUri.toString() - Log.i("[File Viewer] Trying to open file: $filePath") - var type: String? = null - val extension = getExtensionFromFileName(filePath) - - if (extension.isNotEmpty()) { - Log.i("[File Viewer] Found extension $extension") - type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) - } else { - Log.e("[File Viewer] Couldn't find extension") - } - - if (type != null) { - Log.i("[File Viewer] Found matching MIME type $type") - } else { - type = if (extension == "linphonerc") { - "text/plain" - } else { - "file/$extension" - } - Log.w( - "[File Viewer] Can't get MIME type from extension: $extension, will use $type" - ) - } - - intent.setDataAndType(contentUri, type) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - if (newTask) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - - try { - activity.startActivity(intent) - return true - } catch (anfe: ActivityNotFoundException) { - Log.e("[File Viewer] Can't open file in third party app: $anfe") - } - return false - } - - fun openMediaStoreFile( - activity: Activity, - contentFilePath: String, - newTask: Boolean = false - ): Boolean { - val intent = Intent(Intent.ACTION_VIEW) - val contentUri: Uri = Uri.parse(contentFilePath) - intent.data = contentUri - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - if (newTask) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - } - - try { - activity.startActivity(intent) - return true - } catch (anfe: ActivityNotFoundException) { - Log.e("[File Viewer] Can't open media store export in third party app: $anfe") - } - return false - } - - fun writeIntoFile(bytes: ByteArray, file: File) { - val inStream = ByteArrayInputStream(bytes) - val outStream = FileOutputStream(file) - - val buffer = ByteArray(1024) - var read: Int - while (inStream.read(buffer).also { read = it } != -1) { - outStream.write(buffer, 0, read) - } - - inStream.close() - outStream.flush() - outStream.close() - } - - fun countFilesInDirectory(path: String): Int { - val dir = File(path) - if (dir.exists()) { - return dir.listFiles().orEmpty().size - } - return -1 - } - } -} diff --git a/app/src/main/java/org/linphone/utils/ImageUtils.kt b/app/src/main/java/org/linphone/utils/ImageUtils.kt deleted file mode 100644 index 44f0c650b..000000000 --- a/app/src/main/java/org/linphone/utils/ImageUtils.kt +++ /dev/null @@ -1,110 +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 . - */ -package org.linphone.utils - -import android.content.Context -import android.graphics.* -import android.net.Uri -import android.util.Base64 -import java.io.FileNotFoundException -import org.linphone.compatibility.Compatibility -import org.linphone.core.tools.Log - -class ImageUtils { - companion object { - private const val BASE64_REGEX = "^data:image\\/(gif|png|jpeg|bmp|webp|svg\\+xml)(;charset=utf-8)?;base64,[A-Za-z0-9+\\/]+={0,2}\$" - - fun isBase64(source: String): Boolean { - return source.matches(Regex(BASE64_REGEX)) - } - - fun getBase64ImageFromString(base64: String): ByteArray? { - val substring = base64.substring(base64.indexOf(",") + 1) - return Base64.decode(substring, Base64.DEFAULT) - } - - fun getRoundBitmapFromUri( - context: Context, - fromPictureUri: Uri? - ): Bitmap? { - var bm: Bitmap? = null - if (fromPictureUri != null) { - bm = try { - // We make a copy to ensure Bitmap will be Software and not Hardware, required for shortcuts - Compatibility.getBitmapFromUri(context, fromPictureUri).copy( - Bitmap.Config.ARGB_8888, - true - ) - } catch (fnfe: FileNotFoundException) { - return null - } catch (e: Exception) { - Log.e("[Image Utils] Failed to get bitmap from URI [$fromPictureUri]: $e") - return null - } - } - if (bm != null) { - val roundBm = getRoundBitmap(bm) - if (roundBm != null) { - bm.recycle() - return roundBm - } - } - return bm - } - - fun rotateImage(source: Bitmap, angle: Float): Bitmap { - val matrix = Matrix() - matrix.postRotate(angle) - val rotatedBitmap = Bitmap.createBitmap( - source, - 0, - 0, - source.width, - source.height, - matrix, - true - ) - source.recycle() - return rotatedBitmap - } - - private fun getRoundBitmap(bitmap: Bitmap): Bitmap? { - val output = - Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(output) - val color = -0xbdbdbe - val paint = Paint() - val rect = - Rect(0, 0, bitmap.width, bitmap.height) - paint.isAntiAlias = true - canvas.drawARGB(0, 0, 0, 0) - paint.color = color - canvas.drawCircle( - bitmap.width / 2.toFloat(), - bitmap.height / 2.toFloat(), - bitmap.width / 2.toFloat(), - paint - ) - paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) - canvas.drawBitmap(bitmap, rect, rect, paint) - return output - } - } -} diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt deleted file mode 100644 index 85efa1468..000000000 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ /dev/null @@ -1,346 +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 . - */ -package org.linphone.utils - -import android.annotation.SuppressLint -import android.content.Context -import android.net.ConnectivityManager -import android.net.NetworkInfo -import android.telephony.TelephonyManager.* -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException -import java.text.DateFormat -import java.text.SimpleDateFormat -import java.util.* -import okhttp3.internal.and -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences -import org.linphone.R -import org.linphone.core.* -import org.linphone.core.tools.Log - -/** - * Various utility methods for Linphone SDK - */ -class LinphoneUtils { - companion object { - private const val RECORDING_DATE_PATTERN = "dd-MM-yyyy-HH-mm-ss" - - fun getDisplayName(address: Address?): String { - if (address == null) return "[null]" - if (address.displayName == null) { - val account = coreContext.core.accountList.find { account -> - account.params.identityAddress?.asStringUriOnly() == address.asStringUriOnly() - } - val localDisplayName = account?.params?.identityAddress?.displayName - // Do not return an empty local display name - if (!localDisplayName.isNullOrEmpty()) { - return localDisplayName - } - } - // Do not return an empty display name - return address.displayName ?: address.username ?: address.asString() - } - - fun getDisplayableAddress(address: Address?): String { - if (address == null) return "[null]" - return if (corePreferences.replaceSipUriByUsername) { - address.username ?: address.asStringUriOnly() - } else { - val copy = address.clone() - copy.clean() // To remove gruu if any - copy.asStringUriOnly() - } - } - - fun getCleanedAddress(address: Address): Address { - // To remove the GRUU if any - val cleanAddress = address.clone() - cleanAddress.clean() - return cleanAddress - } - - fun getConferenceAddress(call: Call): Address? { - val remoteContact = call.remoteContact - val conferenceAddress = if (call.dir == Call.Dir.Incoming) { - if (remoteContact != null) { - coreContext.core.interpretUrl(remoteContact, false) - } else { - null - } - } else { - call.remoteAddress - } - return conferenceAddress - } - - fun getConferenceSubject(conference: Conference): String? { - return if (conference.subject.isNullOrEmpty()) { - val conferenceAddress = conference.conferenceAddress ?: return conference.subject - val conferenceInfo = coreContext.core.findConferenceInformationFromUri( - conferenceAddress - ) - if (conferenceInfo != null) { - conferenceInfo.subject - } else { - if (conference.me.isFocus) { - AppUtils.getString(R.string.conference_local_title) - } else { - AppUtils.getString(R.string.conference_default_title) - } - } - } else { - conference.subject - } - } - - fun isEndToEndEncryptedChatAvailable(): Boolean { - val core = coreContext.core - return core.isLimeX3DhEnabled && - (core.limeX3DhServerUrl != null || core.defaultAccount?.params?.limeServerUrl != null) && - core.defaultAccount?.params?.conferenceFactoryUri != null - } - - fun isGroupChatAvailable(): Boolean { - val core = coreContext.core - return core.defaultAccount?.params?.conferenceFactoryUri != null - } - - fun isRemoteConferencingAvailable(): Boolean { - val core = coreContext.core - return core.defaultAccount?.params?.audioVideoConferenceFactoryAddress != null - } - - fun createOneToOneChatRoom(participant: Address, isSecured: Boolean = false): ChatRoom? { - val core: Core = coreContext.core - val defaultAccount = core.defaultAccount - - val params = core.createDefaultChatRoomParams() - params.isGroupEnabled = false - params.backend = ChatRoom.Backend.Basic - if (isSecured) { - params.subject = AppUtils.getString(R.string.chat_room_dummy_subject) - params.isEncryptionEnabled = true - params.backend = ChatRoom.Backend.FlexisipChat - } - - val participants = arrayOf(participant) - - return core.searchChatRoom( - params, - defaultAccount?.params?.identityAddress, - null, - participants - ) - ?: core.createChatRoom( - params, - defaultAccount?.params?.identityAddress, - participants - ) - } - - fun getConferenceInvitationsChatRoomParams(): ChatRoomParams { - val chatRoomParams = coreContext.core.createDefaultChatRoomParams() - chatRoomParams.isGroupEnabled = false - if (isEndToEndEncryptedChatAvailable()) { - chatRoomParams.backend = ChatRoom.Backend.FlexisipChat - chatRoomParams.isEncryptionEnabled = true - } else { - chatRoomParams.backend = ChatRoom.Backend.Basic - chatRoomParams.isEncryptionEnabled = false - } - chatRoomParams.subject = "Meeting invitation" // Won't be used - return chatRoomParams - } - - fun deleteFilesAttachedToEventLog(eventLog: EventLog) { - if (eventLog.type == EventLog.Type.ConferenceChatMessage) { - val message = eventLog.chatMessage - if (message != null) deleteFilesAttachedToChatMessage(message) - } - } - - fun deleteFilesAttachedToChatMessage(chatMessage: ChatMessage) { - for (content in chatMessage.contents) { - val filePath = content.filePath - if (!filePath.isNullOrEmpty()) { - Log.i("[Linphone Utils] Deleting file $filePath") - FileUtils.deleteFile(filePath) - } - } - } - - fun getRecordingFilePathForAddress(address: Address): String { - val displayName = getDisplayName(address) - val dateFormat: DateFormat = SimpleDateFormat( - RECORDING_DATE_PATTERN, - Locale.getDefault() - ) - val fileName = "${displayName}_${dateFormat.format(Date())}.mkv" - return FileUtils.getFileStoragePath(fileName).absolutePath - } - - fun getRecordingFilePathForConference(subject: String?): String { - val dateFormat: DateFormat = SimpleDateFormat( - RECORDING_DATE_PATTERN, - Locale.getDefault() - ) - val fileName = if (subject.isNullOrEmpty()) { - "conference_${dateFormat.format(Date())}.mkv" - } else { - "${subject}_${dateFormat.format(Date())}.mkv" - } - return FileUtils.getFileStoragePath(fileName).absolutePath - } - - fun getRecordingDateFromFileName(name: String): Date { - return SimpleDateFormat(RECORDING_DATE_PATTERN, Locale.getDefault()).parse(name) - } - - @SuppressLint("MissingPermission") - fun checkIfNetworkHasLowBandwidth(context: Context): Boolean { - val connMgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager - val networkInfo: NetworkInfo? = connMgr.activeNetworkInfo - if (networkInfo != null && networkInfo.isConnected) { - if (networkInfo.type == ConnectivityManager.TYPE_MOBILE) { - return when (networkInfo.subtype) { - NETWORK_TYPE_EDGE, NETWORK_TYPE_GPRS, NETWORK_TYPE_IDEN -> true - else -> false - } - } - } - // In doubt return false - return false - } - - fun isCallLogMissed(callLog: CallLog): Boolean { - return ( - callLog.dir == Call.Dir.Incoming && - ( - callLog.status == Call.Status.Missed || - callLog.status == Call.Status.Aborted || - callLog.status == Call.Status.EarlyAborted - ) - ) - } - - fun getChatRoomId(room: ChatRoom): String { - return getChatRoomId(room.localAddress, room.peerAddress) - } - - fun getChatRoomId(localAddress: Address, remoteAddress: Address): String { - val localSipUri = localAddress.clone() - localSipUri.clean() - val remoteSipUri = remoteAddress.clone() - remoteSipUri.clean() - return "${localSipUri.asStringUriOnly()}~${remoteSipUri.asStringUriOnly()}" - } - - fun getAccountsNotHidden(): List { - val list = arrayListOf() - - for (account in coreContext.core.accountList) { - if (account.getCustomParam("hidden") != "1") { - list.add(account) - } - } - - return list - } - - fun applyInternationalPrefix(): Boolean { - val account = coreContext.core.defaultAccount - if (account != null) { - val params = account.params - return params.useInternationalPrefixForCallsAndChats - } - - return true // Legacy behavior - } - - fun isPushNotificationAvailable(): Boolean { - val core = coreContext.core - if (!core.isPushNotificationAvailable) { - return false - } - - val pushConfig = core.pushNotificationConfig ?: return false - if (pushConfig.provider.isNullOrEmpty()) return false - if (pushConfig.param.isNullOrEmpty()) return false - if (pushConfig.prid.isNullOrEmpty()) return false - - return true - } - - fun isFileTransferAvailable(): Boolean { - val core = coreContext.core - return core.fileTransferServer.orEmpty().isNotEmpty() - } - - fun hashPassword( - userId: String, - password: String, - realm: String, - algorithm: String = "MD5" - ): String? { - val input = "$userId:$realm:$password" - try { - val digestEngine = MessageDigest.getInstance(algorithm) - val digest = digestEngine.digest(input.toByteArray()) - val hexString = StringBuffer() - for (i in digest.indices) { - hexString.append(Integer.toHexString(digest[i].and(0xFF))) - } - return hexString.toString() - } catch (nsae: NoSuchAlgorithmException) { - Log.e("[Side Menu] Can't compute hash using [$algorithm] algorithm!") - } - - return null - } - - fun getTextDescribingMessage(message: ChatMessage): String { - // If message contains text, then use that - var text = message.contents.find { content -> content.isText }?.utf8Text ?: "" - - if (text.isEmpty()) { - val firstContent = message.contents.firstOrNull() - if (firstContent?.isIcalendar == true) { - text = AppUtils.getString( - R.string.conference_invitation_notification_short_desc - ) - } else if (firstContent?.isVoiceRecording == true) { - text = AppUtils.getString( - R.string.chat_message_voice_recording_notification_short_desc - ) - } else { - for (content in message.contents) { - if (text.isNotEmpty()) { - text += ", " - } - text += content.name - } - } - } - - return text - } - } -} diff --git a/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt b/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt deleted file mode 100644 index 19b1f8735..000000000 --- a/app/src/main/java/org/linphone/utils/PatternClickableSpan.kt +++ /dev/null @@ -1,73 +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 . - */ -package org.linphone.utils - -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.style.ClickableSpan -import android.view.View -import android.widget.TextView -import java.util.regex.Pattern - -class PatternClickableSpan { - private var patterns: ArrayList = ArrayList() - - inner class SpannablePatternItem( - var pattern: Pattern, - var listener: SpannableClickedListener - ) - - interface SpannableClickedListener { - fun onSpanClicked(text: String) - } - - inner class StyledClickableSpan(var item: SpannablePatternItem) : ClickableSpan() { - override fun onClick(widget: View) { - val tv = widget as TextView - val span = tv.text as Spanned - val start = span.getSpanStart(this) - val end = span.getSpanEnd(this) - val text = span.subSequence(start, end) - item.listener.onSpanClicked(text.toString()) - } - } - - fun add( - pattern: Pattern, - listener: SpannableClickedListener - ): PatternClickableSpan { - patterns.add(SpannablePatternItem(pattern, listener)) - return this - } - - fun build(editable: CharSequence?): SpannableStringBuilder { - val ssb = SpannableStringBuilder(editable) - for (item in patterns) { - val matcher = item.pattern.matcher(ssb) - while (matcher.find()) { - val start = matcher.start() - val end = matcher.end() - val url = StyledClickableSpan(item) - ssb.setSpan(url, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } - return ssb - } -} diff --git a/app/src/main/java/org/linphone/utils/PermissionHelper.kt b/app/src/main/java/org/linphone/utils/PermissionHelper.kt deleted file mode 100644 index e50e7d04c..000000000 --- a/app/src/main/java/org/linphone/utils/PermissionHelper.kt +++ /dev/null @@ -1,82 +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 . - */ -package org.linphone.utils - -import android.Manifest -import android.content.Context -import org.linphone.compatibility.Compatibility -import org.linphone.core.tools.Log -import org.linphone.mediastream.Version - -/** - * Helper methods to check whether a permission has been granted and log the result - */ -class PermissionHelper private constructor(private val context: Context) { - companion object : SingletonHolder(::PermissionHelper) - - private fun hasPermission(permission: String): Boolean { - val granted = Compatibility.hasPermission(context, permission) - - if (granted) { - Log.d("[Permission Helper] Permission $permission is granted") - } else { - Log.w("[Permission Helper] Permission $permission is denied") - } - - return granted - } - - fun hasReadContactsPermission(): Boolean { - return hasPermission(Manifest.permission.READ_CONTACTS) - } - - fun hasWriteContactsPermission(): Boolean { - return hasPermission(Manifest.permission.WRITE_CONTACTS) - } - - fun hasReadPhoneStatePermission(): Boolean { - return hasPermission(Manifest.permission.READ_PHONE_STATE) - } - - fun hasReadPhoneStateOrPhoneNumbersPermission(): Boolean { - return Compatibility.hasReadPhoneStateOrNumbersPermission(context) - } - - fun hasReadExternalStoragePermission(): Boolean { - return Compatibility.hasReadExternalStoragePermission(context) - } - - fun hasWriteExternalStoragePermission(): Boolean { - if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10)) return true - return hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } - - fun hasCameraPermission(): Boolean { - return hasPermission(Manifest.permission.CAMERA) - } - - fun hasRecordAudioPermission(): Boolean { - return hasPermission(Manifest.permission.RECORD_AUDIO) - } - - fun hasPostNotificationsPermission(): Boolean { - return Compatibility.hasPostNotificationsPermission(context) - } -} diff --git a/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt b/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt deleted file mode 100644 index 901c4b3c2..000000000 --- a/app/src/main/java/org/linphone/utils/PhoneNumberUtils.kt +++ /dev/null @@ -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 . - */ -package org.linphone.utils - -import android.annotation.SuppressLint -import android.content.Context -import android.content.res.Resources -import android.provider.ContactsContract -import android.telephony.TelephonyManager -import org.linphone.core.DialPlan -import org.linphone.core.Factory -import org.linphone.core.tools.Log - -class PhoneNumberUtils { - companion object { - fun getDialPlanForCurrentCountry(context: Context): DialPlan? { - try { - val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - val countryIso = tm.networkCountryIso - return getDialPlanFromCountryCode(countryIso) - } catch (e: java.lang.Exception) { - Log.e("[Phone Number Utils] $e") - } - return null - } - - @SuppressLint("MissingPermission", "HardwareIds") - fun getDevicePhoneNumber(context: Context): String? { - if (PermissionHelper.get().hasReadPhoneStateOrPhoneNumbersPermission()) { - try { - val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager - return tm.line1Number - } catch (e: java.lang.Exception) { - Log.e("[Phone Number Utils] $e") - } - } - return null - } - - fun getDialPlanFromCountryCallingPrefix(countryCode: String): DialPlan? { - for (c in Factory.instance().dialPlans) { - if (countryCode == c.countryCallingCode) return c - } - return null - } - - fun addressBookLabelTypeToVcardParamString(type: Int, default: String?): String { - return when (type) { - ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT -> "assistant" - ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK -> "callback" - ContactsContract.CommonDataKinds.Phone.TYPE_CAR -> "car" - ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN -> "work,main" - ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME -> "home,fax" - ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK -> "work,fax" - ContactsContract.CommonDataKinds.Phone.TYPE_HOME -> "home" - ContactsContract.CommonDataKinds.Phone.TYPE_ISDN -> "isdn" - ContactsContract.CommonDataKinds.Phone.TYPE_MAIN -> "main" - ContactsContract.CommonDataKinds.Phone.TYPE_MMS -> "text" - ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE -> "cell" - ContactsContract.CommonDataKinds.Phone.TYPE_OTHER -> "other" - ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX -> "fax" - ContactsContract.CommonDataKinds.Phone.TYPE_PAGER -> "pager" - ContactsContract.CommonDataKinds.Phone.TYPE_RADIO -> "radio" - ContactsContract.CommonDataKinds.Phone.TYPE_TELEX -> "telex" - ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD -> "textphone" - ContactsContract.CommonDataKinds.Phone.TYPE_WORK -> "work" - ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE -> "work,cell" - ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER -> "work,pager" - ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM -> default ?: "custom" - else -> default ?: type.toString() - } - } - - fun vcardParamStringToAddressBookLabel(resources: Resources, label: String): String { - if (label.isEmpty()) return label - val type = labelToType(label) - return ContactsContract.CommonDataKinds.Phone.getTypeLabel(resources, type, label).toString() - } - - private fun labelToType(label: String): Int { - return when (label) { - "assistant" -> ContactsContract.CommonDataKinds.Phone.TYPE_ASSISTANT - "callback" -> ContactsContract.CommonDataKinds.Phone.TYPE_CALLBACK - "car" -> ContactsContract.CommonDataKinds.Phone.TYPE_CAR - "work,main" -> ContactsContract.CommonDataKinds.Phone.TYPE_COMPANY_MAIN - "home,fax" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_HOME - "work,fax" -> ContactsContract.CommonDataKinds.Phone.TYPE_FAX_WORK - "home" -> ContactsContract.CommonDataKinds.Phone.TYPE_HOME - "isdn" -> ContactsContract.CommonDataKinds.Phone.TYPE_ISDN - "main" -> ContactsContract.CommonDataKinds.Phone.TYPE_MAIN - "text" -> ContactsContract.CommonDataKinds.Phone.TYPE_MMS - "cell" -> ContactsContract.CommonDataKinds.Phone.TYPE_MOBILE - "other" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER - "fax" -> ContactsContract.CommonDataKinds.Phone.TYPE_OTHER_FAX - "pager" -> ContactsContract.CommonDataKinds.Phone.TYPE_PAGER - "radio" -> ContactsContract.CommonDataKinds.Phone.TYPE_RADIO - "telex" -> ContactsContract.CommonDataKinds.Phone.TYPE_TELEX - "textphone" -> ContactsContract.CommonDataKinds.Phone.TYPE_TTY_TDD - "work" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK - "work,cell" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_MOBILE - "work,pager" -> ContactsContract.CommonDataKinds.Phone.TYPE_WORK_PAGER - "custom" -> ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM - else -> ContactsContract.CommonDataKinds.BaseTypes.TYPE_CUSTOM - } - } - - private fun getDialPlanFromCountryCode(countryCode: String): DialPlan? { - for (c in Factory.instance().dialPlans) { - if (countryCode.equals(c.isoCountryCode, ignoreCase = true)) return c - } - return null - } - - fun arePhoneNumberWeakEqual(number1: String, number2: String): Boolean { - return trimPhoneNumber(number1) == trimPhoneNumber(number2) - } - - private fun trimPhoneNumber(phoneNumber: String): String { - return phoneNumber.replace(" ", "") - .replace("-", "") - .replace("(", "") - .replace(")", "") - } - } -} diff --git a/app/src/main/java/org/linphone/utils/PowerManagerUtils.kt b/app/src/main/java/org/linphone/utils/PowerManagerUtils.kt deleted file mode 100644 index f54c7d44a..000000000 --- a/app/src/main/java/org/linphone/utils/PowerManagerUtils.kt +++ /dev/null @@ -1,163 +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 . - */ -package org.linphone.utils - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import android.content.pm.ResolveInfo -import android.net.Uri - -class PowerManagerUtils { - companion object { - // https://stackoverflow.com/questions/31638986/protected-apps-setting-on-huawei-phones-and-how-to-handle-it - private val POWER_MANAGER_INTENTS = arrayOf( - Intent() - .setComponent( - ComponentName( - "com.miui.securitycenter", - "com.miui.permcenter.autostart.AutoStartManagementActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.letv.android.letvsafe", - "com.letv.android.letvsafe.AutobootManageActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.huawei.systemmanager", - "com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.huawei.systemmanager", - "com.huawei.systemmanager.optimize.process.ProtectActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.coloros.safecenter", - "com.coloros.safecenter.permission.startup.StartupAppListActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.coloros.safecenter", - "com.coloros.safecenter.startupapp.StartupAppListActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.oppo.safe", - "com.oppo.safe.permission.startup.StartupAppListActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.iqoo.secure", - "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.iqoo.secure", - "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.vivo.permissionmanager", - "com.vivo.permissionmanager.activity.BgStartUpManagerActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.samsung.android.lool", - "com.samsung.android.sm.ui.battery.BatteryActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.htc.pitroad", - "com.htc.pitroad.landingpage.activity.LandingPageActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.asus.mobilemanager", - "com.asus.mobilemanager.MainActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.asus.mobilemanager", - "com.asus.mobilemanager.autostart.AutoStartActivity" - ) - ), - Intent() - .setComponent( - ComponentName( - "com.asus.mobilemanager", - "com.asus.mobilemanager.entry.FunctionActivity" - ) - ) - .setData(Uri.parse("mobilemanager://function/entry/AutoStart")), - Intent() - .setComponent( - ComponentName( - "com.dewav.dwappmanager", - "com.dewav.dwappmanager.memory.SmartClearupWhiteList" - ) - ) - ) - - fun getDevicePowerManagerIntent(context: Context): Intent? { - for (intent in POWER_MANAGER_INTENTS) { - if (isIntentCallable(context, intent)) { - return intent - } - } - return null - } - - private fun isIntentCallable(context: Context, intent: Intent): Boolean { - val list: List = context.packageManager - .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) - return list.isNotEmpty() - } - } -} diff --git a/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt b/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt deleted file mode 100644 index 4b598a4e7..000000000 --- a/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt +++ /dev/null @@ -1,98 +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 . - */ -package org.linphone.utils - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Rect -import android.util.SparseArray -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView - -class RecyclerViewHeaderDecoration(private val context: Context, private val adapter: HeaderAdapter) : RecyclerView.ItemDecoration() { - private val headers: SparseArray = SparseArray() - - override fun getItemOffsets( - outRect: Rect, - view: View, - parent: RecyclerView, - state: RecyclerView.State - ) { - val position = (view.layoutParams as RecyclerView.LayoutParams).bindingAdapterPosition - - if (position != RecyclerView.NO_POSITION && adapter.displayHeaderForPosition(position)) { - val headerView: View = adapter.getHeaderViewForPosition(view.context, position) - headers.put(position, headerView) - measureHeaderView(headerView, parent) - outRect.top = headerView.height - } else { - headers.remove(position) - } - } - - private fun measureHeaderView(view: View, parent: ViewGroup) { - if (view.layoutParams == null) { - view.layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - - val widthSpec = View.MeasureSpec.makeMeasureSpec(parent.width, View.MeasureSpec.EXACTLY) - val heightSpec = View.MeasureSpec.makeMeasureSpec(parent.height, View.MeasureSpec.EXACTLY) - val childWidth = ViewGroup.getChildMeasureSpec( - widthSpec, - parent.paddingLeft + parent.paddingRight, - view.layoutParams.width - ) - val childHeight = ViewGroup.getChildMeasureSpec( - heightSpec, - parent.paddingTop + parent.paddingBottom, - view.layoutParams.height - ) - - view.measure(childWidth, childHeight) - view.layout(0, 0, view.measuredWidth, view.measuredHeight) - } - - override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { - for (i in 0 until parent.childCount) { - val child = parent.getChildAt(i) - val position = parent.getChildAdapterPosition(child) - if (position != RecyclerView.NO_POSITION && adapter.displayHeaderForPosition(position)) { - canvas.save() - val headerView: View = headers.get(position) ?: adapter.getHeaderViewForPosition( - context, - position - ) - canvas.translate(0f, child.y - headerView.height) - headerView.draw(canvas) - canvas.restore() - } - } - } -} - -interface HeaderAdapter { - fun displayHeaderForPosition(position: Int): Boolean - - fun getHeaderViewForPosition(context: Context, position: Int): View -} diff --git a/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt b/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt deleted file mode 100644 index 764e7dbc0..000000000 --- a/app/src/main/java/org/linphone/utils/RecyclerViewSwipeUtils.kt +++ /dev/null @@ -1,344 +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 . - */ -package org.linphone.utils - -import android.graphics.Canvas -import android.graphics.Color -import android.graphics.PorterDuff -import android.graphics.Typeface -import android.graphics.drawable.ColorDrawable -import android.text.TextPaint -import android.util.TypedValue -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import org.linphone.core.tools.Log - -/** - * Helper class to properly display swipe actions in list items. - */ -class RecyclerViewSwipeUtils( - direction: Int, - configuration: RecyclerViewSwipeConfiguration, - listener: RecyclerViewSwipeListener -) : ItemTouchHelper(RecyclerViewSwipeUtilsCallback(direction, configuration, listener)) - -class RecyclerViewSwipeConfiguration { - class Action( - val text: String = "", - val textColor: Int = Color.WHITE, - val backgroundColor: Int = 0, - val icon: Int = 0, - val iconTint: Int = 0, - val preventFor: Class<*>? = null - ) - - val iconMargin = 16f - - val actionTextSizeUnit = TypedValue.COMPLEX_UNIT_SP - - // At least CROSSCALL Action-X3 device doesn't have SANS_SERIF typeface... - val actionTextFont: Typeface? = Typeface.SANS_SERIF - val actionTextSize = 14f - - var leftToRightAction = Action() - var rightToLeftAction = Action() -} - -private class RecyclerViewSwipeUtilsCallback( - val direction: Int, - val configuration: RecyclerViewSwipeConfiguration, - val listener: RecyclerViewSwipeListener -) : ItemTouchHelper.SimpleCallback(0, direction) { - - fun leftToRightSwipe( - canvas: Canvas, - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - dX: Float - ) { - if (configuration.leftToRightAction.backgroundColor != 0) { - val background = ColorDrawable(configuration.leftToRightAction.backgroundColor) - background.setBounds( - viewHolder.itemView.left, - viewHolder.itemView.top, - viewHolder.itemView.left + dX.toInt(), - viewHolder.itemView.bottom - ) - background.draw(canvas) - } - - val horizontalMargin: Int = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - configuration.iconMargin, - recyclerView.context.resources.displayMetrics - ).toInt() - var iconWidth = 0 - - if (configuration.leftToRightAction.icon != 0) { - val icon = - ContextCompat.getDrawable( - recyclerView.context, - configuration.leftToRightAction.icon - ) - iconWidth = icon?.intrinsicWidth ?: 0 - if (icon != null && dX > iconWidth) { - val halfIcon = icon.intrinsicHeight / 2 - val top = - viewHolder.itemView.top + ((viewHolder.itemView.bottom - viewHolder.itemView.top) / 2 - halfIcon) - - // Icon won't move past the swipe threshold, thus indicating to the user - // it has reached the required distance for swipe action to be done - val threshold = getSwipeThreshold(viewHolder) * viewHolder.itemView.right - val left = if (dX < threshold) { - viewHolder.itemView.left + dX.toInt() - iconWidth - } else { - viewHolder.itemView.left + threshold.toInt() - iconWidth - } - - icon.setBounds( - left, - top, - left + iconWidth, - top + icon.intrinsicHeight - ) - - @Suppress("DEPRECATION") - if (configuration.leftToRightAction.iconTint != 0) { - icon.setColorFilter( - configuration.leftToRightAction.iconTint, - PorterDuff.Mode.SRC_IN - ) - } - icon.draw(canvas) - } - } - - if (configuration.leftToRightAction.text.isNotEmpty() && dX > horizontalMargin + iconWidth) { - val textPaint = TextPaint() - textPaint.isAntiAlias = true - textPaint.textSize = TypedValue.applyDimension( - configuration.actionTextSizeUnit, - configuration.actionTextSize, - recyclerView.context.resources.displayMetrics - ) - textPaint.color = configuration.leftToRightAction.textColor - textPaint.typeface = configuration.actionTextFont - - val margin = if (iconWidth > 0) horizontalMargin / 2 else 0 - val textX = - (viewHolder.itemView.left + horizontalMargin + iconWidth + margin).toFloat() - val textY = - (viewHolder.itemView.top + (viewHolder.itemView.bottom - viewHolder.itemView.top) / 2.0 + textPaint.textSize / 2).toFloat() - canvas.drawText( - configuration.leftToRightAction.text, - textX, - textY, - textPaint - ) - } - } - - fun rightToLeftSwipe( - canvas: Canvas, - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - dX: Float - ) { - if (configuration.rightToLeftAction.backgroundColor != 0) { - val background = ColorDrawable(configuration.rightToLeftAction.backgroundColor) - background.setBounds( - viewHolder.itemView.right + dX.toInt(), - viewHolder.itemView.top, - viewHolder.itemView.right, - viewHolder.itemView.bottom - ) - background.draw(canvas) - } - - val horizontalMargin: Int = TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, - configuration.iconMargin, - recyclerView.context.resources.displayMetrics - ).toInt() - var iconWidth = 0 - var imageLeftBorder = viewHolder.itemView.right - - if (configuration.rightToLeftAction.icon != 0) { - val icon = - ContextCompat.getDrawable( - recyclerView.context, - configuration.rightToLeftAction.icon - ) - iconWidth = icon?.intrinsicWidth ?: 0 - if (icon != null && dX < viewHolder.itemView.right - iconWidth) { - val halfIcon = icon.intrinsicHeight / 2 - val top = - viewHolder.itemView.top + ((viewHolder.itemView.bottom - viewHolder.itemView.top) / 2 - halfIcon) - - // Icon won't move past the swipe threshold, thus indicating to the user - // it has reached the required distance for swipe action to be done - val threshold = -(getSwipeThreshold(viewHolder) * viewHolder.itemView.right) - val right = if (dX > threshold) { - viewHolder.itemView.right + dX.toInt() - } else { - viewHolder.itemView.right + threshold.toInt() - } - imageLeftBorder = right - icon.intrinsicWidth - - icon.setBounds( - imageLeftBorder, - top, - right, - top + icon.intrinsicHeight - ) - - @Suppress("DEPRECATION") - if (configuration.rightToLeftAction.iconTint != 0) { - icon.setColorFilter( - configuration.rightToLeftAction.iconTint, - PorterDuff.Mode.SRC_IN - ) - } - icon.draw(canvas) - } - } - - if (configuration.rightToLeftAction.text.isNotEmpty() && dX < -horizontalMargin - iconWidth) { - val textPaint = TextPaint() - textPaint.isAntiAlias = true - textPaint.textSize = TypedValue.applyDimension( - configuration.actionTextSizeUnit, - configuration.actionTextSize, - recyclerView.context.resources.displayMetrics - ) - textPaint.color = configuration.rightToLeftAction.textColor - textPaint.typeface = configuration.actionTextFont - - val margin = - if (imageLeftBorder == viewHolder.itemView.right) horizontalMargin else horizontalMargin / 2 - val textX = - imageLeftBorder - textPaint.measureText(configuration.rightToLeftAction.text) - margin - val textY = - (viewHolder.itemView.top + (viewHolder.itemView.bottom - viewHolder.itemView.top) / 2.0 + textPaint.textSize / 2).toFloat() - canvas.drawText( - configuration.rightToLeftAction.text, - textX, - textY, - textPaint - ) - } - } - - fun applyConfiguration( - canvas: Canvas, - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - dX: Float, - actionState: Int - ) { - try { - if (actionState != ItemTouchHelper.ACTION_STATE_SWIPE) return - - if (dX > 0) { - leftToRightSwipe(canvas, recyclerView, viewHolder, dX) - } else if (dX < 0) { - rightToLeftSwipe(canvas, recyclerView, viewHolder, dX) - } - } catch (e: Exception) { - Log.e("[RecyclerView Swipe Utils] $e") - } - } - - override fun getSwipeDirs( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ): Int { - // Prevent swipe actions for a specific ViewHolder class if needed - // Used to allow swipe actions on chat messages but not events - var dirFlags = direction - if (direction and ItemTouchHelper.RIGHT != 0) { - val classToPrevent = configuration.leftToRightAction.preventFor - if (classToPrevent != null) { - if (classToPrevent.isInstance(viewHolder)) { - dirFlags = dirFlags and ItemTouchHelper.RIGHT.inv() - } - } - } - if (direction or ItemTouchHelper.LEFT != 0) { - val classToPrevent = configuration.rightToLeftAction.preventFor - if (classToPrevent != null) { - if (classToPrevent.isInstance(viewHolder)) { - dirFlags = dirFlags and ItemTouchHelper.LEFT.inv() - } - } - } - return dirFlags - } - - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - return false - } - - override fun onSwiped( - viewHolder: RecyclerView.ViewHolder, - direction: Int - ) { - if (direction == ItemTouchHelper.LEFT) { - listener.onRightToLeftSwipe(viewHolder) - } else if (direction == ItemTouchHelper.RIGHT) { - listener.onLeftToRightSwipe(viewHolder) - } - } - - override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { - return .33f // A third of the screen is required to validate swipe move (default is .5f) - } - - override fun onChildDraw( - canvas: Canvas, - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - dX: Float, - dY: Float, - actionState: Int, - isCurrentlyActive: Boolean - ) { - applyConfiguration(canvas, recyclerView, viewHolder, dX, actionState) - super.onChildDraw( - canvas, - recyclerView, - viewHolder, - dX, - dY, - actionState, - isCurrentlyActive - ) - } -} - -interface RecyclerViewSwipeListener { - fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) - fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) -} diff --git a/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt b/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt deleted file mode 100644 index 65ed4dd19..000000000 --- a/app/src/main/java/org/linphone/utils/ShortcutsHelper.kt +++ /dev/null @@ -1,247 +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 . - */ -package org.linphone.utils - -import android.annotation.TargetApi -import android.content.Context -import android.content.Intent -import android.content.pm.ShortcutInfo -import android.os.Bundle -import androidx.collection.ArraySet -import androidx.core.app.Person -import androidx.core.content.LocusIdCompat -import androidx.core.content.pm.ShortcutInfoCompat -import androidx.core.content.pm.ShortcutManagerCompat -import androidx.core.graphics.drawable.IconCompat -import kotlin.math.min -import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.activities.main.MainActivity -import org.linphone.contact.getPerson -import org.linphone.core.Address -import org.linphone.core.ChatRoom -import org.linphone.core.ChatRoom.Capabilities -import org.linphone.core.Friend -import org.linphone.core.tools.Log -import org.linphone.mediastream.Version - -@TargetApi(25) -class ShortcutsHelper(val context: Context) { - companion object { - fun createShortcutsToContacts(context: Context) { - val shortcuts = ArrayList() - if (ShortcutManagerCompat.isRateLimitingActive(context)) { - Log.e("[Shortcut Helper] Rate limiting is active, aborting") - return - } - - val maxShortcuts = min(ShortcutManagerCompat.getMaxShortcutCountPerActivity(context), 5) - var count = 0 - val processedAddresses = arrayListOf() - for (room in coreContext.core.chatRooms) { - // Android can usually only have around 4-5 shortcuts at a time - if (count >= maxShortcuts) { - Log.w("[Shortcut Helper] Max amount of shortcuts reached ($count)") - break - } - - val addresses: ArrayList
= arrayListOf(room.peerAddress) - if (!room.hasCapability(Capabilities.Basic.toInt())) { - addresses.clear() - for (participant in room.participants) { - addresses.add(participant.address) - } - } - for (address in addresses) { - if (count >= maxShortcuts) { - Log.w("[Shortcut Helper] Max amount of shortcuts reached ($count)") - break - } - - val stringAddress = address.asStringUriOnly() - if (!processedAddresses.contains(stringAddress)) { - processedAddresses.add(stringAddress) - val contact: Friend? = - coreContext.contactsManager.findContactByAddress(address) - - if (contact != null && contact.refKey != null) { - val shortcut: ShortcutInfoCompat? = createContactShortcut( - context, - contact - ) - if (shortcut != null) { - Log.i( - "[Shortcut Helper] Creating launcher shortcut for ${shortcut.shortLabel}" - ) - shortcuts.add(shortcut) - count += 1 - } - } else { - Log.w("[Shortcut Helper] Contact not found for address: $stringAddress") - } - } - } - } - ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts) - } - - private fun createContactShortcut(context: Context, contact: Friend): ShortcutInfoCompat? { - try { - val categories: ArraySet = ArraySet() - categories.add(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION) - - val person = contact.getPerson() - val icon = person.icon - val id = contact.refKey ?: return null - - val intent = Intent(Intent.ACTION_MAIN) - intent.setClass(context, MainActivity::class.java) - intent.putExtra("ContactId", id) - - return ShortcutInfoCompat.Builder(context, id) - .setShortLabel(contact.name ?: "") - .setIcon(icon) - .setPerson(person) - .setCategories(categories) - .setIntent(intent) - .build() - } catch (e: Exception) { - Log.e( - "[Shortcuts Helper] createContactShortcut for contact [${contact.name}] exception: $e" - ) - } - - return null - } - - fun createShortcutsToChatRooms(context: Context) { - val shortcuts = ArrayList() - if (ShortcutManagerCompat.isRateLimitingActive(context)) { - Log.e("[Shortcut Helper] Rate limiting is active, aborting") - return - } - Log.i("[Shortcut Helper] Creating launcher shortcuts for chat rooms") - val maxShortcuts = min(ShortcutManagerCompat.getMaxShortcutCountPerActivity(context), 5) - var count = 0 - for (room in coreContext.core.chatRooms) { - // Android can usually only have around 4-5 shortcuts at a time - if (count >= maxShortcuts) { - Log.w("[Shortcut Helper] Max amount of shortcuts reached ($count)") - break - } - - val shortcut: ShortcutInfoCompat? = createChatRoomShortcut(context, room) - if (shortcut != null) { - Log.i("[Shortcut Helper] Created launcher shortcut for ${shortcut.shortLabel}") - shortcuts.add(shortcut) - count += 1 - } - } - ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts) - Log.i("[Shortcut Helper] Created $count launcher shortcuts") - } - - private fun createChatRoomShortcut(context: Context, chatRoom: ChatRoom): ShortcutInfoCompat? { - val localAddress = chatRoom.localAddress.asStringUriOnly() - val peerAddress = chatRoom.peerAddress.asStringUriOnly() - val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress) - - try { - val categories: ArraySet = ArraySet() - categories.add(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION) - - val personsList = arrayListOf() - val subject: String - val icon: IconCompat - if (chatRoom.hasCapability(Capabilities.Basic.toInt())) { - val contact = - coreContext.contactsManager.findContactByAddress(chatRoom.peerAddress) - val person = contact?.getPerson() - if (person != null) { - personsList.add(person) - } - - icon = person?.icon ?: coreContext.contactsManager.contactAvatar - subject = contact?.name ?: LinphoneUtils.getDisplayName(chatRoom.peerAddress) - } else if (chatRoom.hasCapability(Capabilities.OneToOne.toInt()) && chatRoom.participants.isNotEmpty()) { - val address = chatRoom.participants.first().address - val contact = - coreContext.contactsManager.findContactByAddress(address) - val person = contact?.getPerson() - if (person != null) { - personsList.add(person) - } - - subject = contact?.name ?: LinphoneUtils.getDisplayName(address) - icon = person?.icon ?: coreContext.contactsManager.contactAvatar - } else { - for (participant in chatRoom.participants) { - val contact = - coreContext.contactsManager.findContactByAddress(participant.address) - if (contact != null) { - personsList.add(contact.getPerson()) - } - } - subject = chatRoom.subject.orEmpty() - icon = coreContext.contactsManager.groupAvatar - } - - val persons = arrayOfNulls(personsList.size) - personsList.toArray(persons) - - val args = Bundle() - args.putString("RemoteSipUri", peerAddress) - args.putString("LocalSipUri", localAddress) - - val intent = Intent(Intent.ACTION_MAIN) - intent.setClass(context, MainActivity::class.java) - intent.putExtra("Chat", true) - intent.putExtra("RemoteSipUri", peerAddress) - intent.putExtra("LocalSipUri", localAddress) - - return ShortcutInfoCompat.Builder(context, id) - .setShortLabel(subject) - .setIcon(icon) - .setPersons(persons) - .setCategories(categories) - .setIntent(intent) - .setLongLived(Version.sdkAboveOrEqual(Version.API30_ANDROID_11)) - .setLocusId(LocusIdCompat(id)) - .build() - } catch (e: Exception) { - Log.e("[Shortcuts Helper] createChatRoomShortcut for id [$id] exception: $e") - } - - return null - } - - fun removeShortcuts(context: Context) { - Log.w("[Shortcut Helper] Removing all contacts shortcuts") - ShortcutManagerCompat.removeAllDynamicShortcuts(context) - } - - fun isShortcutToChatRoomAlreadyCreated(context: Context, chatRoom: ChatRoom): Boolean { - val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress) - val found = ShortcutManagerCompat.getDynamicShortcuts(context).find { - it.id == id - } - return found != null - } - } -} diff --git a/app/src/main/java/org/linphone/utils/SingletonHolder.kt b/app/src/main/java/org/linphone/utils/SingletonHolder.kt deleted file mode 100644 index 379ba4e3a..000000000 --- a/app/src/main/java/org/linphone/utils/SingletonHolder.kt +++ /dev/null @@ -1,62 +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 . - */ -package org.linphone.utils - -/** - * Helper class to create singletons like CoreContext. - */ -open class SingletonHolder(val creator: (A) -> T) { - @Volatile private var instance: T? = null - - fun exists(): Boolean { - return instance != null - } - - fun destroy() { - instance = null - } - - fun get(): T { - // Will throw NPE if needed - return instance!! - } - - fun create(arg: A): T { - val i = instance - if (i != null) { - return i - } - - return synchronized(this) { - val i2 = instance - if (i2 != null) { - i2 - } else { - val created = creator(arg) - instance = created - created - } - } - } - - fun required(arg: A): T { - return instance ?: create(arg) - } -} diff --git a/app/src/main/java/org/linphone/utils/TimestampUtils.kt b/app/src/main/java/org/linphone/utils/TimestampUtils.kt deleted file mode 100644 index baf0e7262..000000000 --- a/app/src/main/java/org/linphone/utils/TimestampUtils.kt +++ /dev/null @@ -1,174 +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 . - */ -package org.linphone.utils - -import java.text.DateFormat -import java.text.Format -import java.text.SimpleDateFormat -import java.util.* -import org.linphone.LinphoneApplication - -class TimestampUtils { - companion object { - fun isToday(timestamp: Long, timestampInSecs: Boolean = true): Boolean { - val cal = Calendar.getInstance() - cal.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp - return isSameDay(cal, Calendar.getInstance()) - } - - fun isYesterday(timestamp: Long, timestampInSecs: Boolean = true): Boolean { - val yesterday = Calendar.getInstance() - yesterday.roll(Calendar.DAY_OF_MONTH, -1) - val cal = Calendar.getInstance() - cal.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp - return isSameDay(cal, yesterday) - } - - fun isSameDay(timestamp1: Long, timestamp2: Long, timestampInSecs: Boolean = true): Boolean { - val cal1 = Calendar.getInstance() - cal1.timeInMillis = if (timestampInSecs) timestamp1 * 1000 else timestamp1 - val cal2 = Calendar.getInstance() - cal2.timeInMillis = if (timestampInSecs) timestamp2 * 1000 else timestamp2 - return isSameDay(cal1, cal2) - } - - fun isSameDay( - cal1: Date, - cal2: Date - ): Boolean { - return isSameDay(cal1.time, cal2.time, false) - } - - fun dateToString(date: Long, timestampInSecs: Boolean = true): String { - val dateFormat: Format = android.text.format.DateFormat.getDateFormat( - LinphoneApplication.coreContext.context - ) - val pattern = (dateFormat as SimpleDateFormat).toLocalizedPattern() - - val calendar = Calendar.getInstance() - calendar.timeInMillis = if (timestampInSecs) date * 1000 else date - - // See https://github.com/material-components/material-components-android/issues/882 - val dateFormatter = SimpleDateFormat(pattern, Locale.getDefault()) - dateFormatter.timeZone = TimeZone.getTimeZone("UTC") - return dateFormatter.format(calendar.time) - } - - fun timeToString(hour: Int, minutes: Int): String { - val use24hFormat = android.text.format.DateFormat.is24HourFormat( - LinphoneApplication.coreContext.context - ) - val calendar = Calendar.getInstance() - calendar.set(Calendar.HOUR_OF_DAY, hour) - calendar.set(Calendar.MINUTE, minutes) - - return if (use24hFormat) { - SimpleDateFormat("HH'h'mm", Locale.getDefault()).format(calendar.time) - } else { - SimpleDateFormat("h:mm a", Locale.getDefault()).format(calendar.time) - } - } - - fun timeToString(time: Long, timestampInSecs: Boolean = true): String { - val use24hFormat = android.text.format.DateFormat.is24HourFormat( - LinphoneApplication.coreContext.context - ) - val calendar = Calendar.getInstance() - calendar.timeInMillis = if (timestampInSecs) time * 1000 else time - - return if (use24hFormat) { - SimpleDateFormat("HH'h'mm", Locale.getDefault()).format(calendar.time) - } else { - SimpleDateFormat("h:mm a", Locale.getDefault()).format(calendar.time) - } - } - - fun durationToString(hours: Int, minutes: Int): String { - val calendar = Calendar.getInstance() - calendar.set(Calendar.HOUR_OF_DAY, hours) - calendar.set(Calendar.MINUTE, minutes) - val pattern = when { - hours == 0 -> "mm'min'" - hours < 10 && minutes == 0 -> "H'h'" - hours < 10 && minutes > 0 -> "H'h'mm" - hours >= 10 && minutes == 0 -> "HH'h'" - else -> "HH'h'mm" - } - return SimpleDateFormat(pattern, Locale.getDefault()).format(calendar.time) - } - - private fun isSameYear(timestamp: Long, timestampInSecs: Boolean = true): Boolean { - val cal = Calendar.getInstance() - cal.timeInMillis = if (timestampInSecs) timestamp * 1000 else timestamp - return isSameYear(cal, Calendar.getInstance()) - } - - fun toString( - timestamp: Long, - onlyDate: Boolean = false, - timestampInSecs: Boolean = true, - shortDate: Boolean = true, - hideYear: Boolean = true - ): String { - val dateFormat = if (isToday(timestamp, timestampInSecs)) { - DateFormat.getTimeInstance(DateFormat.SHORT) - } else { - if (onlyDate) { - DateFormat.getDateInstance(if (shortDate) DateFormat.SHORT else DateFormat.FULL) - } else { - DateFormat.getDateTimeInstance( - if (shortDate) DateFormat.SHORT else DateFormat.MEDIUM, - DateFormat.SHORT - ) - } - } as SimpleDateFormat - - if (hideYear || isSameYear(timestamp, timestampInSecs)) { - // Remove the year part of the format - dateFormat.applyPattern( - dateFormat.toPattern().replace( - "/?y+/?|,?\\s?y+\\s?".toRegex(), - if (shortDate) "" else " " - ) - ) - } - - val millis = if (timestampInSecs) timestamp * 1000 else timestamp - return dateFormat.format(Date(millis)).capitalize(Locale.getDefault()) - } - - private fun isSameDay( - cal1: Calendar, - cal2: Calendar - ): Boolean { - return cal1[Calendar.ERA] == cal2[Calendar.ERA] && - cal1[Calendar.YEAR] == cal2[Calendar.YEAR] && - cal1[Calendar.DAY_OF_YEAR] == cal2[Calendar.DAY_OF_YEAR] - } - - private fun isSameYear( - cal1: Calendar, - cal2: Calendar - ): Boolean { - return cal1[Calendar.ERA] == cal2[Calendar.ERA] && - cal1[Calendar.YEAR] == cal2[Calendar.YEAR] - } - } -} diff --git a/app/src/main/java/org/linphone/views/MarqueeTextView.kt b/app/src/main/java/org/linphone/views/MarqueeTextView.kt deleted file mode 100644 index 55cd5d3eb..000000000 --- a/app/src/main/java/org/linphone/views/MarqueeTextView.kt +++ /dev/null @@ -1,52 +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 . - */ -package org.linphone.views - -import android.content.Context -import android.text.TextUtils -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatTextView - -/** - * The purpose of this class is to have a TextView automatically configured for marquee ellipsize. - */ -class MarqueeTextView : AppCompatTextView { - constructor(context: Context) : super(context) { - init() - } - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - init() - } - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init() - } - - fun init() { - ellipsize = TextUtils.TruncateAt.MARQUEE - marqueeRepeatLimit = -0x1 - isSelected = true - } -} diff --git a/app/src/main/java/org/linphone/views/SettingTextInputEditText.kt b/app/src/main/java/org/linphone/views/SettingTextInputEditText.kt deleted file mode 100644 index 6340f2896..000000000 --- a/app/src/main/java/org/linphone/views/SettingTextInputEditText.kt +++ /dev/null @@ -1,54 +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 . - */ -package org.linphone.views - -import android.app.Activity -import android.content.Context -import android.util.AttributeSet -import android.view.inputmethod.InputMethodManager -import com.google.android.material.textfield.TextInputEditText -import org.linphone.activities.main.settings.SettingListener - -class SettingTextInputEditText : TextInputEditText { - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) - - fun fakeImeDone(listener: SettingListener) { - listener.onTextValueChanged(text.toString()) - - // Send IME action DONE to trigger onSettingImeDone binding adapter, but that doesn't work... - // val inputConnection = BaseInputConnection(this, true) - // inputConnection.performEditorAction(EditorInfo.IME_ACTION_DONE) - - // Will make check icon to disappear thanks to onFocusChangeVisibilityOf binding adapter - clearFocus() - - // Hide keyboard - val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager - imm.hideSoftInputFromWindow(windowToken, 0) - } -} diff --git a/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt b/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt deleted file mode 100644 index 619717f28..000000000 --- a/app/src/main/java/org/linphone/views/VoiceRecordProgressBar.kt +++ /dev/null @@ -1,352 +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 . - */ -package org.linphone.views - -import android.content.Context -import android.graphics.Canvas -import android.graphics.Rect -import android.graphics.drawable.* -import android.os.Parcel -import android.os.Parcelable -import android.util.AttributeSet -import android.view.View -import org.linphone.R - -class VoiceRecordProgressBar : View { - companion object { - const val MAX_LEVEL = 10000 - } - - private var minWidth = 0 - private var maxWidth = 0 - private var minHeight = 0 - private var maxHeight = 0 - - private var progress = 0 - private var secondaryProgress = 0 - private var max = 0 - private var progressDrawable: Drawable? = null - - private var primaryLeftMargin: Float = 0f - private var primaryRightMargin: Float = 0f - - constructor(context: Context) : this(context, null) - - constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) - - constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super( - context, - attrs, - defStyle - ) { - max = 100 - progress = 0 - secondaryProgress = 0 - minWidth = 24 - maxWidth = 48 - minHeight = 24 - maxHeight = 48 - - context.theme.obtainStyledAttributes( - attrs, - R.styleable.VoiceRecordProgressBar, - 0, - 0 - ).apply { - - try { - val drawable = getDrawable(R.styleable.VoiceRecordProgressBar_progressDrawable) - if (drawable != null) { - setProgressDrawable(drawable) - } - setPrimaryLeftMargin( - getDimension(R.styleable.VoiceRecordProgressBar_primaryLeftMargin, 0f) - ) - setPrimaryRightMargin( - getDimension(R.styleable.VoiceRecordProgressBar_primaryRightMargin, 0f) - ) - setMax(getInteger(R.styleable.VoiceRecordProgressBar_max, 100)) - } finally { - recycle() - } - } - - setProgress(0) - setSecondaryProgress(0) - } - - override fun onSaveInstanceState(): Parcelable { - val superState: Parcelable? = super.onSaveInstanceState() - val savedState = SavedState(superState) - savedState.max = max - savedState.progress = progress - savedState.secondaryProgress = secondaryProgress - return savedState - } - - override fun onRestoreInstanceState(state: Parcelable) { - val savedState = state as SavedState - super.onRestoreInstanceState(savedState.superState) - setMax(savedState.max) - setProgress(savedState.progress) - setSecondaryProgress(savedState.secondaryProgress) - } - - override fun drawableStateChanged() { - super.drawableStateChanged() - updateDrawableState() - } - - override fun invalidateDrawable(drawable: Drawable) { - if (verifyDrawable(drawable)) { - val dirty: Rect = drawable.bounds - val scrollX: Int = scrollX + paddingLeft - val scrollY: Int = scrollY + paddingTop - invalidate( - dirty.left + scrollX, - dirty.top + scrollY, - dirty.right + scrollX, - dirty.bottom + scrollY - ) - } else { - super.invalidateDrawable(drawable) - } - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val drawable: Drawable? = progressDrawable - var dw = 0 - var dh = 0 - - if (drawable != null) { - dw = minWidth.coerceAtLeast(maxWidth.coerceAtMost(drawable.intrinsicWidth)) - dh = minHeight.coerceAtLeast(maxHeight.coerceAtMost(drawable.intrinsicHeight)) - } - - updateDrawableState() - dw += paddingRight + paddingLeft - dh += paddingBottom + paddingTop - - setMeasuredDimension( - resolveSizeAndState(dw, widthMeasureSpec, 0), - resolveSizeAndState(dh, heightMeasureSpec, 0) - ) - } - - override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { - updateDrawableBounds(w, h) - } - - override fun verifyDrawable(who: Drawable): Boolean { - return who === progressDrawable || super.verifyDrawable(who) - } - - override fun jumpDrawablesToCurrentState() { - super.jumpDrawablesToCurrentState() - progressDrawable?.jumpToCurrentState() - } - - private fun setPrimaryLeftMargin(margin: Float) { - primaryLeftMargin = margin - } - - private fun setPrimaryRightMargin(margin: Float) { - primaryRightMargin = margin - } - - fun setProgress(p: Int) { - var progress = p - if (progress < 0) { - progress = 0 - } - if (progress > max) { - progress = max - } - - if (progress != this.progress) { - this.progress = progress - refreshProgress(android.R.id.progress, this.progress) - } - } - - fun setSecondaryProgress(sp: Int) { - var secondaryProgress = sp - if (secondaryProgress < 0) { - secondaryProgress = 0 - } - if (secondaryProgress > max) { - secondaryProgress = max - } - - if (secondaryProgress != this.secondaryProgress) { - this.secondaryProgress = secondaryProgress - refreshProgress(android.R.id.secondaryProgress, this.secondaryProgress) - } - } - - fun setMax(m: Int) { - var max = m - if (max < 0) { - max = 0 - } - if (max != this.max) { - this.max = max - postInvalidate() - if (progress > max) { - progress = max - } - refreshProgress(android.R.id.progress, progress) - } - } - - fun setSecondaryProgressTint(color: Int) { - val drawable = progressDrawable - if (drawable != null) { - if (drawable is LayerDrawable) { - val secondaryProgressDrawable = drawable.findDrawableByLayerId( - android.R.id.secondaryProgress - ) - secondaryProgressDrawable?.setTint(color) - } - } - } - - private fun setProgressDrawable(drawable: Drawable) { - val needUpdate: Boolean = if (progressDrawable != null && drawable !== progressDrawable) { - progressDrawable?.callback = null - true - } else { - false - } - - drawable.callback = this - // Make sure the ProgressBar is always tall enough - val drawableHeight = drawable.minimumHeight - if (maxHeight < drawableHeight) { - maxHeight = drawableHeight - requestLayout() - } - - progressDrawable = drawable - postInvalidate() - - if (needUpdate) { - updateDrawableBounds(width, height) - updateDrawableState() - - refreshProgress(android.R.id.progress, progress) - refreshProgress(android.R.id.secondaryProgress, secondaryProgress) - } - } - - private fun refreshProgress(id: Int, progress: Int) { - var scale: Float = if (max > 0) (progress.toFloat() / max) else 0f - - if (id == android.R.id.progress && scale > 0) { - if (width > 0) { - // Wait for secondaryProgress to have reached primaryLeftMargin to start primaryProgress at 0 - val leftOffset = primaryLeftMargin / width - if (scale < leftOffset) return - - // Prevent primaryProgress to go further than (width - rightMargin) - val rightOffset = primaryRightMargin / width - if (scale > 1 - rightOffset) { - scale = 1 - rightOffset - } - - // Remove left margin from primary progress - scale -= leftOffset - - // Since we use setBounds() to apply margins to the Bitmaps, - // the width of the bitmap is reduced so we have to adapt the level - val widthScale = width - (primaryLeftMargin + primaryRightMargin) - scale = ((scale * width) / widthScale) - } - } - - val drawable: Drawable? = progressDrawable - if (drawable != null) { - var progressDrawable: Drawable? = null - if (drawable is LayerDrawable) { - progressDrawable = drawable.findDrawableByLayerId(id) - } - (progressDrawable ?: drawable).level = (scale * MAX_LEVEL).toInt() - } else { - invalidate() - } - } - - private fun updateDrawableState() { - val state = drawableState - if (progressDrawable != null && progressDrawable?.isStateful == true) { - progressDrawable?.state = state - } - } - - private fun updateDrawableBounds(w: Int, h: Int) { - val right: Int = w - paddingRight - paddingLeft - val bottom: Int = h - paddingBottom - paddingTop - progressDrawable?.setBounds(0, 0, right, bottom) - } - - @Synchronized - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - val drawable = progressDrawable as? LayerDrawable - - if (drawable != null) { - canvas.save() - canvas.translate(paddingLeft.toFloat(), paddingTop.toFloat()) - - for (i in 0 until drawable.numberOfLayers) { - val drawableLayer = drawable.getDrawable(i) - if (i != 1) { - canvas.translate(primaryLeftMargin, 0f) - drawableLayer.draw(canvas) - drawableLayer.setBounds( - 0, - 0, - width - primaryRightMargin.toInt() - primaryLeftMargin.toInt(), - height - ) - canvas.translate(-primaryLeftMargin, 0f) - } else { - drawableLayer.draw(canvas) - } - } - - canvas.restore() - } - } - - internal class SavedState(superState: Parcelable?) : BaseSavedState(superState) { - var max = 0 - var progress = 0 - var secondaryProgress = 0 - - override fun writeToParcel(output: Parcel, flags: Int) { - super.writeToParcel(output, flags) - - output.writeInt(max) - output.writeInt(progress) - output.writeInt(secondaryProgress) - } - } -} diff --git a/app/src/main/res/color/dark_primary_text_color.xml b/app/src/main/res/color/dark_primary_text_color.xml deleted file mode 100644 index a0eaff7cb..000000000 --- a/app/src/main/res/color/dark_primary_text_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/light_primary_text_color.xml b/app/src/main/res/color/light_primary_text_color.xml deleted file mode 100644 index 13b5f1ece..000000000 --- a/app/src/main/res/color/light_primary_text_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/voip_extra_button_text_color.xml b/app/src/main/res/color/voip_extra_button_text_color.xml deleted file mode 100644 index 336ccd7b2..000000000 --- a/app/src/main/res/color/voip_extra_button_text_color.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable-hdpi/topbar_call_notification.png b/app/src/main/res/drawable-hdpi/topbar_call_notification.png deleted file mode 100644 index ab24cd5c1..000000000 Binary files a/app/src/main/res/drawable-hdpi/topbar_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/topbar_call_paused_notification.png b/app/src/main/res/drawable-hdpi/topbar_call_paused_notification.png deleted file mode 100644 index 8718035ef..000000000 Binary files a/app/src/main/res/drawable-hdpi/topbar_call_paused_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/topbar_chat_notification.png b/app/src/main/res/drawable-hdpi/topbar_chat_notification.png deleted file mode 100644 index 975acd34e..000000000 Binary files a/app/src/main/res/drawable-hdpi/topbar_chat_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/topbar_missed_call_notification.png b/app/src/main/res/drawable-hdpi/topbar_missed_call_notification.png deleted file mode 100644 index 8f4ac5aed..000000000 Binary files a/app/src/main/res/drawable-hdpi/topbar_missed_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/topbar_service_notification.png b/app/src/main/res/drawable-hdpi/topbar_service_notification.png deleted file mode 100644 index f4c3cf5c4..000000000 Binary files a/app/src/main/res/drawable-hdpi/topbar_service_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/topbar_videocall_notification.png b/app/src/main/res/drawable-hdpi/topbar_videocall_notification.png deleted file mode 100644 index acb444856..000000000 Binary files a/app/src/main/res/drawable-hdpi/topbar_videocall_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/topbar_call_notification.png b/app/src/main/res/drawable-mdpi/topbar_call_notification.png deleted file mode 100644 index adf504c79..000000000 Binary files a/app/src/main/res/drawable-mdpi/topbar_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/topbar_call_paused_notification.png b/app/src/main/res/drawable-mdpi/topbar_call_paused_notification.png deleted file mode 100644 index 63fff65b9..000000000 Binary files a/app/src/main/res/drawable-mdpi/topbar_call_paused_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/topbar_chat_notification.png b/app/src/main/res/drawable-mdpi/topbar_chat_notification.png deleted file mode 100644 index 618d4869c..000000000 Binary files a/app/src/main/res/drawable-mdpi/topbar_chat_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/topbar_missed_call_notification.png b/app/src/main/res/drawable-mdpi/topbar_missed_call_notification.png deleted file mode 100644 index bdb453946..000000000 Binary files a/app/src/main/res/drawable-mdpi/topbar_missed_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/topbar_service_notification.png b/app/src/main/res/drawable-mdpi/topbar_service_notification.png deleted file mode 100644 index 20cce4f92..000000000 Binary files a/app/src/main/res/drawable-mdpi/topbar_service_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/topbar_videocall_notification.png b/app/src/main/res/drawable-mdpi/topbar_videocall_notification.png deleted file mode 100644 index 87bce3552..000000000 Binary files a/app/src/main/res/drawable-mdpi/topbar_videocall_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/audio_recording_default.png b/app/src/main/res/drawable-xhdpi/audio_recording_default.png deleted file mode 100644 index 6e800a6bc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/audio_recording_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/audio_recording_reply_preview_default.png b/app/src/main/res/drawable-xhdpi/audio_recording_reply_preview_default.png deleted file mode 100644 index e4a9b2087..000000000 Binary files a/app/src/main/res/drawable-xhdpi/audio_recording_reply_preview_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/back_default.png b/app/src/main/res/drawable-xhdpi/back_default.png deleted file mode 100644 index b81b9fb89..000000000 Binary files a/app/src/main/res/drawable-xhdpi/back_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/backspace_default.png b/app/src/main/res/drawable-xhdpi/backspace_default.png deleted file mode 100644 index 1280f1698..000000000 Binary files a/app/src/main/res/drawable-xhdpi/backspace_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_add.png b/app/src/main/res/drawable-xhdpi/call_add.png deleted file mode 100644 index db4ca84b4..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_add.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_alt_start_default.png b/app/src/main/res/drawable-xhdpi/call_alt_start_default.png deleted file mode 100644 index 836a0ec5d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_alt_start_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_audio_start.png b/app/src/main/res/drawable-xhdpi/call_audio_start.png deleted file mode 100644 index 3f7b6932d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_audio_start.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_back_default.png b/app/src/main/res/drawable-xhdpi/call_back_default.png deleted file mode 100644 index 3045bd866..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_back_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_hangup.png b/app/src/main/res/drawable-xhdpi/call_hangup.png deleted file mode 100644 index 23fb40172..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_hangup.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_incoming.png b/app/src/main/res/drawable-xhdpi/call_incoming.png deleted file mode 100644 index 89d593a4c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_incoming.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_missed.png b/app/src/main/res/drawable-xhdpi/call_missed.png deleted file mode 100644 index e1074ffaf..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_missed.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_outgoing.png b/app/src/main/res/drawable-xhdpi/call_outgoing.png deleted file mode 100644 index c6df94bd9..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_outgoing.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_quality_indicator_0.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_0.png deleted file mode 100644 index 06804eda3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_quality_indicator_0.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_quality_indicator_1.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_1.png deleted file mode 100644 index faa61c3e2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_quality_indicator_1.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_quality_indicator_2.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_2.png deleted file mode 100644 index 6bf7b78f6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_quality_indicator_2.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_quality_indicator_3.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_3.png deleted file mode 100644 index a9a2fa03e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_quality_indicator_3.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_quality_indicator_4.png b/app/src/main/res/drawable-xhdpi/call_quality_indicator_4.png deleted file mode 100644 index e2c95d4f7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_quality_indicator_4.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_start_default.png b/app/src/main/res/drawable-xhdpi/call_start_default.png deleted file mode 100644 index bcbd2e068..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_start_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_status_incoming.png b/app/src/main/res/drawable-xhdpi/call_status_incoming.png deleted file mode 100644 index bb3f4d0ff..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_status_incoming.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_status_missed.png b/app/src/main/res/drawable-xhdpi/call_status_missed.png deleted file mode 100644 index 582040717..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_status_missed.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_status_outgoing.png b/app/src/main/res/drawable-xhdpi/call_status_outgoing.png deleted file mode 100644 index 5ae14394b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_status_outgoing.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_transfer.png b/app/src/main/res/drawable-xhdpi/call_transfer.png deleted file mode 100644 index ef831ef39..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_transfer.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/call_video_start.png b/app/src/main/res/drawable-xhdpi/call_video_start.png deleted file mode 100644 index a936b8894..000000000 Binary files a/app/src/main/res/drawable-xhdpi/call_video_start.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/camera_switch_default.png b/app/src/main/res/drawable-xhdpi/camera_switch_default.png deleted file mode 100644 index 532cf3187..000000000 Binary files a/app/src/main/res/drawable-xhdpi/camera_switch_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_delivered.png b/app/src/main/res/drawable-xhdpi/chat_delivered.png deleted file mode 100644 index b49e4e7e8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_delivered.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_error.png b/app/src/main/res/drawable-xhdpi/chat_error.png deleted file mode 100644 index 1979db520..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_error.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_file_over.png b/app/src/main/res/drawable-xhdpi/chat_file_over.png deleted file mode 100644 index b36547ca1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_file_over.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_group_add_default.png b/app/src/main/res/drawable-xhdpi/chat_group_add_default.png deleted file mode 100644 index 54a8954b9..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_group_add_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_group_delete.png b/app/src/main/res/drawable-xhdpi/chat_group_delete.png deleted file mode 100644 index 18b42abed..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_group_delete.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_group_new_default.png b/app/src/main/res/drawable-xhdpi/chat_group_new_default.png deleted file mode 100644 index 7fbb9ae8f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_group_new_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_new_default.png b/app/src/main/res/drawable-xhdpi/chat_new_default.png deleted file mode 100644 index 7d94f9738..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_new_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_read.png b/app/src/main/res/drawable-xhdpi/chat_read.png deleted file mode 100644 index 5c00f33e6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_read.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_send_over.png b/app/src/main/res/drawable-xhdpi/chat_send_over.png deleted file mode 100644 index cce92a4b9..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_send_over.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chat_start_default.png b/app/src/main/res/drawable-xhdpi/chat_start_default.png deleted file mode 100644 index 3bb7892f9..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chat_start_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/check_selected.png b/app/src/main/res/drawable-xhdpi/check_selected.png deleted file mode 100644 index 07298e894..000000000 Binary files a/app/src/main/res/drawable-xhdpi/check_selected.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/check_unselected.png b/app/src/main/res/drawable-xhdpi/check_unselected.png deleted file mode 100644 index 3e8704632..000000000 Binary files a/app/src/main/res/drawable-xhdpi/check_unselected.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chevron_list_close_default.png b/app/src/main/res/drawable-xhdpi/chevron_list_close_default.png deleted file mode 100644 index 80de198cc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chevron_list_close_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/chevron_list_open_default.png b/app/src/main/res/drawable-xhdpi/chevron_list_open_default.png deleted file mode 100644 index 422f69f9c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/chevron_list_open_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/collapse_default.png b/app/src/main/res/drawable-xhdpi/collapse_default.png deleted file mode 100644 index c6895edc8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/collapse_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/conference_schedule_calendar_default.png b/app/src/main/res/drawable-xhdpi/conference_schedule_calendar_default.png deleted file mode 100644 index 59fe950fb..000000000 Binary files a/app/src/main/res/drawable-xhdpi/conference_schedule_calendar_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/conference_schedule_participants_default.png b/app/src/main/res/drawable-xhdpi/conference_schedule_participants_default.png deleted file mode 100644 index a58af55e1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/conference_schedule_participants_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/conference_schedule_time_default.png b/app/src/main/res/drawable-xhdpi/conference_schedule_time_default.png deleted file mode 100644 index 7a9534617..000000000 Binary files a/app/src/main/res/drawable-xhdpi/conference_schedule_time_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/contact_add_default.png b/app/src/main/res/drawable-xhdpi/contact_add_default.png deleted file mode 100644 index 83c8f26df..000000000 Binary files a/app/src/main/res/drawable-xhdpi/contact_add_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/contact_default.png b/app/src/main/res/drawable-xhdpi/contact_default.png deleted file mode 100644 index f2abc78db..000000000 Binary files a/app/src/main/res/drawable-xhdpi/contact_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/contacts_all_default.png b/app/src/main/res/drawable-xhdpi/contacts_all_default.png deleted file mode 100644 index eb98ed424..000000000 Binary files a/app/src/main/res/drawable-xhdpi/contacts_all_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/contacts_sip_default.png b/app/src/main/res/drawable-xhdpi/contacts_sip_default.png deleted file mode 100644 index 2dad4eb12..000000000 Binary files a/app/src/main/res/drawable-xhdpi/contacts_sip_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/delete_default.png b/app/src/main/res/drawable-xhdpi/delete_default.png deleted file mode 100644 index 76e3d112a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/delete_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/deselect_all_default.png b/app/src/main/res/drawable-xhdpi/deselect_all_default.png deleted file mode 100644 index 69ee8a272..000000000 Binary files a/app/src/main/res/drawable-xhdpi/deselect_all_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/dialer_background.png b/app/src/main/res/drawable-xhdpi/dialer_background.png deleted file mode 100644 index ccb0eedd9..000000000 Binary files a/app/src/main/res/drawable-xhdpi/dialer_background.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/edit_default.png b/app/src/main/res/drawable-xhdpi/edit_default.png deleted file mode 100644 index 5550b05a2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/edit_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/emoji_default.png b/app/src/main/res/drawable-xhdpi/emoji_default.png deleted file mode 100644 index 563340737..000000000 Binary files a/app/src/main/res/drawable-xhdpi/emoji_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ephemeral_messages_default.png b/app/src/main/res/drawable-xhdpi/ephemeral_messages_default.png deleted file mode 100644 index a9acdf1f3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/ephemeral_messages_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/file_audio_default.png b/app/src/main/res/drawable-xhdpi/file_audio_default.png deleted file mode 100644 index 7cdd59372..000000000 Binary files a/app/src/main/res/drawable-xhdpi/file_audio_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/file_default.png b/app/src/main/res/drawable-xhdpi/file_default.png deleted file mode 100644 index 086edeacc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/file_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/file_pdf_default.png b/app/src/main/res/drawable-xhdpi/file_pdf_default.png deleted file mode 100644 index eca17905f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/file_pdf_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/file_picture_default.png b/app/src/main/res/drawable-xhdpi/file_picture_default.png deleted file mode 100644 index 2bee7e7df..000000000 Binary files a/app/src/main/res/drawable-xhdpi/file_picture_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/file_video_default.png b/app/src/main/res/drawable-xhdpi/file_video_default.png deleted file mode 100644 index f08f014f3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/file_video_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/footer_chat.png b/app/src/main/res/drawable-xhdpi/footer_chat.png deleted file mode 100644 index 08baaa4bc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/footer_chat.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/footer_contacts.png b/app/src/main/res/drawable-xhdpi/footer_contacts.png deleted file mode 100644 index fe35fecbf..000000000 Binary files a/app/src/main/res/drawable-xhdpi/footer_contacts.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/footer_dialer.png b/app/src/main/res/drawable-xhdpi/footer_dialer.png deleted file mode 100644 index d76f053e3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/footer_dialer.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/footer_history.png b/app/src/main/res/drawable-xhdpi/footer_history.png deleted file mode 100644 index 96bfde0b6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/footer_history.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/forward_message_default.png b/app/src/main/res/drawable-xhdpi/forward_message_default.png deleted file mode 100644 index b51542ec8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/forward_message_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/forwarded_message_default.png b/app/src/main/res/drawable-xhdpi/forwarded_message_default.png deleted file mode 100644 index 55c5d617f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/forwarded_message_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/fullscreen_default.png b/app/src/main/res/drawable-xhdpi/fullscreen_default.png deleted file mode 100644 index afbb2de2e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/fullscreen_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/history_all_default.png b/app/src/main/res/drawable-xhdpi/history_all_default.png deleted file mode 100644 index 867e2cf70..000000000 Binary files a/app/src/main/res/drawable-xhdpi/history_all_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/history_missed_default.png b/app/src/main/res/drawable-xhdpi/history_missed_default.png deleted file mode 100644 index 0ccad7b92..000000000 Binary files a/app/src/main/res/drawable-xhdpi/history_missed_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/info.png b/app/src/main/res/drawable-xhdpi/info.png deleted file mode 100644 index 22ba01c96..000000000 Binary files a/app/src/main/res/drawable-xhdpi/info.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/linphone_app_name_logo.png b/app/src/main/res/drawable-xhdpi/linphone_app_name_logo.png deleted file mode 100644 index bfef888d2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/linphone_app_name_logo.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/linphone_logo.png b/app/src/main/res/drawable-xhdpi/linphone_logo.png deleted file mode 100644 index 533a93450..000000000 Binary files a/app/src/main/res/drawable-xhdpi/linphone_logo.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/list_details_default.png b/app/src/main/res/drawable-xhdpi/list_details_default.png deleted file mode 100644 index 756af521b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/list_details_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_about_default.png b/app/src/main/res/drawable-xhdpi/menu_about_default.png deleted file mode 100644 index 37b24bbaa..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_about_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_add_contact.png b/app/src/main/res/drawable-xhdpi/menu_add_contact.png deleted file mode 100644 index 3ac3f3574..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_add_contact.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_assistant_default.png b/app/src/main/res/drawable-xhdpi/menu_assistant_default.png deleted file mode 100644 index 8b6955242..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_assistant_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_contact.png b/app/src/main/res/drawable-xhdpi/menu_contact.png deleted file mode 100644 index 8bbe253df..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_contact.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_copy_text_default.png b/app/src/main/res/drawable-xhdpi/menu_copy_text_default.png deleted file mode 100644 index ac945fb5e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_copy_text_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_default.png b/app/src/main/res/drawable-xhdpi/menu_default.png deleted file mode 100644 index d203ff475..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_delete_default.png b/app/src/main/res/drawable-xhdpi/menu_delete_default.png deleted file mode 100644 index 65d612aae..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_delete_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_ephemeral_messages_default.png b/app/src/main/res/drawable-xhdpi/menu_ephemeral_messages_default.png deleted file mode 100644 index bbe180afd..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_ephemeral_messages_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_forward_default.png b/app/src/main/res/drawable-xhdpi/menu_forward_default.png deleted file mode 100644 index 9a5b9d85f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_forward_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_group_info_default.png b/app/src/main/res/drawable-xhdpi/menu_group_info_default.png deleted file mode 100644 index 249d74a56..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_group_info_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_meeting_schedule.png b/app/src/main/res/drawable-xhdpi/menu_meeting_schedule.png deleted file mode 100644 index 64ba07792..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_meeting_schedule.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_notifications_off.png b/app/src/main/res/drawable-xhdpi/menu_notifications_off.png deleted file mode 100644 index 556e26a8b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_notifications_off.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_notifications_on.png b/app/src/main/res/drawable-xhdpi/menu_notifications_on.png deleted file mode 100644 index d9fa816ad..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_notifications_on.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_options_default.png b/app/src/main/res/drawable-xhdpi/menu_options_default.png deleted file mode 100644 index 3a1c7800f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_options_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_recordings_default.png b/app/src/main/res/drawable-xhdpi/menu_recordings_default.png deleted file mode 100644 index 3bef81d13..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_recordings_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_reply_default.png b/app/src/main/res/drawable-xhdpi/menu_reply_default.png deleted file mode 100644 index 7f898c47e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_reply_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/menu_security_default.png b/app/src/main/res/drawable-xhdpi/menu_security_default.png deleted file mode 100644 index 477de590a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/menu_security_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/next_default.png b/app/src/main/res/drawable-xhdpi/next_default.png deleted file mode 100644 index 3cea216fb..000000000 Binary files a/app/src/main/res/drawable-xhdpi/next_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_0.png b/app/src/main/res/drawable-xhdpi/numpad_0.png deleted file mode 100644 index 9e1e99f0e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_0.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_1.png b/app/src/main/res/drawable-xhdpi/numpad_1.png deleted file mode 100644 index 8a0e0ec82..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_1.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_2.png b/app/src/main/res/drawable-xhdpi/numpad_2.png deleted file mode 100644 index 4d5e7a30a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_2.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_3.png b/app/src/main/res/drawable-xhdpi/numpad_3.png deleted file mode 100644 index 0c9976674..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_3.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_4.png b/app/src/main/res/drawable-xhdpi/numpad_4.png deleted file mode 100644 index ac4f7811a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_4.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_5.png b/app/src/main/res/drawable-xhdpi/numpad_5.png deleted file mode 100644 index 1edd6d68c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_5.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_6.png b/app/src/main/res/drawable-xhdpi/numpad_6.png deleted file mode 100644 index 83f40dc0b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_6.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_7.png b/app/src/main/res/drawable-xhdpi/numpad_7.png deleted file mode 100644 index 0cab6cb80..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_7.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_8.png b/app/src/main/res/drawable-xhdpi/numpad_8.png deleted file mode 100644 index 1e1aac033..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_8.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_9.png b/app/src/main/res/drawable-xhdpi/numpad_9.png deleted file mode 100644 index db29328f2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_hash.png b/app/src/main/res/drawable-xhdpi/numpad_hash.png deleted file mode 100644 index 48ca9dae2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_hash.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/numpad_star.png b/app/src/main/res/drawable-xhdpi/numpad_star.png deleted file mode 100644 index 5eda5fb98..000000000 Binary files a/app/src/main/res/drawable-xhdpi/numpad_star.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/quit_default.png b/app/src/main/res/drawable-xhdpi/quit_default.png deleted file mode 100644 index a8896a98a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/quit_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/record_audio_message_default.png b/app/src/main/res/drawable-xhdpi/record_audio_message_default.png deleted file mode 100644 index 715ff2bb1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/record_audio_message_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/record_pause_default.png b/app/src/main/res/drawable-xhdpi/record_pause_default.png deleted file mode 100644 index 15e1af51f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/record_pause_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/record_play_default.png b/app/src/main/res/drawable-xhdpi/record_play_default.png deleted file mode 100644 index 134279fc0..000000000 Binary files a/app/src/main/res/drawable-xhdpi/record_play_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/record_stop_default.png b/app/src/main/res/drawable-xhdpi/record_stop_default.png deleted file mode 100644 index 14f700c3d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/record_stop_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/replied_message_default.png b/app/src/main/res/drawable-xhdpi/replied_message_default.png deleted file mode 100644 index de2fe7649..000000000 Binary files a/app/src/main/res/drawable-xhdpi/replied_message_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/resizable_assistant_button_default.9.png b/app/src/main/res/drawable-xhdpi/resizable_assistant_button_default.9.png deleted file mode 100644 index cc63e9cdd..000000000 Binary files a/app/src/main/res/drawable-xhdpi/resizable_assistant_button_default.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/resizable_assistant_button_disabled.9.png b/app/src/main/res/drawable-xhdpi/resizable_assistant_button_disabled.9.png deleted file mode 100644 index df69f9217..000000000 Binary files a/app/src/main/res/drawable-xhdpi/resizable_assistant_button_disabled.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/resizable_assistant_button_over.9.png b/app/src/main/res/drawable-xhdpi/resizable_assistant_button_over.9.png deleted file mode 100644 index d8c4b0ee2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/resizable_assistant_button_over.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/resizable_text_field.9.png b/app/src/main/res/drawable-xhdpi/resizable_text_field.9.png deleted file mode 100644 index 51ad75cf4..000000000 Binary files a/app/src/main/res/drawable-xhdpi/resizable_text_field.9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/scroll_to_bottom_default.png b/app/src/main/res/drawable-xhdpi/scroll_to_bottom_default.png deleted file mode 100644 index 3be410ae1..000000000 Binary files a/app/src/main/res/drawable-xhdpi/scroll_to_bottom_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/search.png b/app/src/main/res/drawable-xhdpi/search.png deleted file mode 100644 index c487f5a1c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/search.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/security_1_indicator.png b/app/src/main/res/drawable-xhdpi/security_1_indicator.png deleted file mode 100644 index 0aa4a2e04..000000000 Binary files a/app/src/main/res/drawable-xhdpi/security_1_indicator.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/security_2_indicator.png b/app/src/main/res/drawable-xhdpi/security_2_indicator.png deleted file mode 100644 index 910e3d124..000000000 Binary files a/app/src/main/res/drawable-xhdpi/security_2_indicator.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/security_alert_indicator.png b/app/src/main/res/drawable-xhdpi/security_alert_indicator.png deleted file mode 100644 index 54a340667..000000000 Binary files a/app/src/main/res/drawable-xhdpi/security_alert_indicator.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/security_ko.png b/app/src/main/res/drawable-xhdpi/security_ko.png deleted file mode 100644 index 9bd476a71..000000000 Binary files a/app/src/main/res/drawable-xhdpi/security_ko.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/security_ok.png b/app/src/main/res/drawable-xhdpi/security_ok.png deleted file mode 100644 index 9ca4a19f2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/security_ok.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/security_pending.png b/app/src/main/res/drawable-xhdpi/security_pending.png deleted file mode 100644 index aa7081af8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/security_pending.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/security_post_quantum.png b/app/src/main/res/drawable-xhdpi/security_post_quantum.png deleted file mode 100644 index 9be1c4cdb..000000000 Binary files a/app/src/main/res/drawable-xhdpi/security_post_quantum.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/security_toggle_icon_green.png b/app/src/main/res/drawable-xhdpi/security_toggle_icon_green.png deleted file mode 100644 index 368e30c79..000000000 Binary files a/app/src/main/res/drawable-xhdpi/security_toggle_icon_green.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/security_toggle_icon_grey.png b/app/src/main/res/drawable-xhdpi/security_toggle_icon_grey.png deleted file mode 100644 index c77235794..000000000 Binary files a/app/src/main/res/drawable-xhdpi/security_toggle_icon_grey.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/select_all_default.png b/app/src/main/res/drawable-xhdpi/select_all_default.png deleted file mode 100644 index cefe12239..000000000 Binary files a/app/src/main/res/drawable-xhdpi/select_all_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_advanced_default.png b/app/src/main/res/drawable-xhdpi/settings_advanced_default.png deleted file mode 100644 index 099a7770e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/settings_advanced_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_audio_default.png b/app/src/main/res/drawable-xhdpi/settings_audio_default.png deleted file mode 100644 index 4072d173f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/settings_audio_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_call_default.png b/app/src/main/res/drawable-xhdpi/settings_call_default.png deleted file mode 100644 index 951452780..000000000 Binary files a/app/src/main/res/drawable-xhdpi/settings_call_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_chat_default.png b/app/src/main/res/drawable-xhdpi/settings_chat_default.png deleted file mode 100644 index 8a3f155ee..000000000 Binary files a/app/src/main/res/drawable-xhdpi/settings_chat_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_conferences_default.png b/app/src/main/res/drawable-xhdpi/settings_conferences_default.png deleted file mode 100644 index 97a08dba0..000000000 Binary files a/app/src/main/res/drawable-xhdpi/settings_conferences_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_contacts_default.png b/app/src/main/res/drawable-xhdpi/settings_contacts_default.png deleted file mode 100644 index 3f034f8e3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/settings_contacts_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_network_default.png b/app/src/main/res/drawable-xhdpi/settings_network_default.png deleted file mode 100644 index dbafc78d7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/settings_network_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/settings_video_default.png b/app/src/main/res/drawable-xhdpi/settings_video_default.png deleted file mode 100644 index 876270d33..000000000 Binary files a/app/src/main/res/drawable-xhdpi/settings_video_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/splashscreen_branding.png b/app/src/main/res/drawable-xhdpi/splashscreen_branding.png deleted file mode 100644 index 99ec7945b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/splashscreen_branding.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/topbar_call_notification.png b/app/src/main/res/drawable-xhdpi/topbar_call_notification.png deleted file mode 100644 index eedae47f2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/topbar_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/topbar_call_paused_notification.png b/app/src/main/res/drawable-xhdpi/topbar_call_paused_notification.png deleted file mode 100644 index dc53b1382..000000000 Binary files a/app/src/main/res/drawable-xhdpi/topbar_call_paused_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/topbar_chat_notification.png b/app/src/main/res/drawable-xhdpi/topbar_chat_notification.png deleted file mode 100644 index b21cc706c..000000000 Binary files a/app/src/main/res/drawable-xhdpi/topbar_chat_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/topbar_missed_call_notification.png b/app/src/main/res/drawable-xhdpi/topbar_missed_call_notification.png deleted file mode 100644 index da9fbf0d4..000000000 Binary files a/app/src/main/res/drawable-xhdpi/topbar_missed_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/topbar_service_notification.png b/app/src/main/res/drawable-xhdpi/topbar_service_notification.png deleted file mode 100644 index 8e3d1a11f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/topbar_service_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/topbar_videocall_notification.png b/app/src/main/res/drawable-xhdpi/topbar_videocall_notification.png deleted file mode 100644 index ca72e9d31..000000000 Binary files a/app/src/main/res/drawable-xhdpi/topbar_videocall_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/valid_default.png b/app/src/main/res/drawable-xhdpi/valid_default.png deleted file mode 100644 index 52bc36535..000000000 Binary files a/app/src/main/res/drawable-xhdpi/valid_default.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voicemail.png b/app/src/main/res/drawable-xhdpi/voicemail.png deleted file mode 100644 index 63d880993..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voicemail.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_audio_routes.png b/app/src/main/res/drawable-xhdpi/voip_audio_routes.png deleted file mode 100644 index fae4e84e3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_audio_routes.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_bluetooth.png b/app/src/main/res/drawable-xhdpi/voip_bluetooth.png deleted file mode 100644 index 3010fb418..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_bluetooth.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call.png b/app/src/main/res/drawable-xhdpi/voip_call.png deleted file mode 100644 index 1a95c052e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_add.png b/app/src/main/res/drawable-xhdpi/voip_call_add.png deleted file mode 100644 index 8237f3305..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_add.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_chat.png b/app/src/main/res/drawable-xhdpi/voip_call_chat.png deleted file mode 100644 index be4a138de..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_chat.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_forward.png b/app/src/main/res/drawable-xhdpi/voip_call_forward.png deleted file mode 100644 index cc61de482..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_forward.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_header_active.png b/app/src/main/res/drawable-xhdpi/voip_call_header_active.png deleted file mode 100644 index 3ef0106bc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_header_active.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_header_incoming.png b/app/src/main/res/drawable-xhdpi/voip_call_header_incoming.png deleted file mode 100644 index 7a8d458ad..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_header_incoming.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_header_outgoing.png b/app/src/main/res/drawable-xhdpi/voip_call_header_outgoing.png deleted file mode 100644 index 474abe754..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_header_outgoing.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_header_paused.png b/app/src/main/res/drawable-xhdpi/voip_call_header_paused.png deleted file mode 100644 index fdcfaf5d2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_header_paused.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_list_menu.png b/app/src/main/res/drawable-xhdpi/voip_call_list_menu.png deleted file mode 100644 index b4e2ff3bf..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_list_menu.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_more.png b/app/src/main/res/drawable-xhdpi/voip_call_more.png deleted file mode 100644 index 73bb13b36..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_more.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_numpad.png b/app/src/main/res/drawable-xhdpi/voip_call_numpad.png deleted file mode 100644 index f6e562387..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_numpad.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_participants.png b/app/src/main/res/drawable-xhdpi/voip_call_participants.png deleted file mode 100644 index b2b766b14..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_participants.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_record.png b/app/src/main/res/drawable-xhdpi/voip_call_record.png deleted file mode 100644 index 9c392dc47..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_record.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_call_stats.png b/app/src/main/res/drawable-xhdpi/voip_call_stats.png deleted file mode 100644 index 018f605f6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_call_stats.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_calls_list.png b/app/src/main/res/drawable-xhdpi/voip_calls_list.png deleted file mode 100644 index a3d69b84e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_calls_list.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_camera_off.png b/app/src/main/res/drawable-xhdpi/voip_camera_off.png deleted file mode 100644 index dd41c338b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_camera_off.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_camera_on.png b/app/src/main/res/drawable-xhdpi/voip_camera_on.png deleted file mode 100644 index 7109c577d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_camera_on.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_cancel.png b/app/src/main/res/drawable-xhdpi/voip_cancel.png deleted file mode 100644 index 493b35e79..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_cancel.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_change_camera.png b/app/src/main/res/drawable-xhdpi/voip_change_camera.png deleted file mode 100644 index d6dc15cb8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_change_camera.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_chat_rooms_list.png b/app/src/main/res/drawable-xhdpi/voip_chat_rooms_list.png deleted file mode 100644 index edf722c7e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_chat_rooms_list.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference.png b/app/src/main/res/drawable-xhdpi/voip_conference.png deleted file mode 100644 index a2bbc39e6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_conference.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_active_speaker.png b/app/src/main/res/drawable-xhdpi/voip_conference_active_speaker.png deleted file mode 100644 index 18f21106a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_conference_active_speaker.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_audio_only.png b/app/src/main/res/drawable-xhdpi/voip_conference_audio_only.png deleted file mode 100644 index fd57a3f27..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_conference_audio_only.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_mosaic.png b/app/src/main/res/drawable-xhdpi/voip_conference_mosaic.png deleted file mode 100644 index 8fa0137b7..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_conference_mosaic.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_paused_big.png b/app/src/main/res/drawable-xhdpi/voip_conference_paused_big.png deleted file mode 100644 index 745f17220..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_conference_paused_big.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_conference_play_big.png b/app/src/main/res/drawable-xhdpi/voip_conference_play_big.png deleted file mode 100644 index 303d05faa..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_conference_play_big.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_copy.png b/app/src/main/res/drawable-xhdpi/voip_copy.png deleted file mode 100644 index 43639693e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_copy.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_delete.png b/app/src/main/res/drawable-xhdpi/voip_delete.png deleted file mode 100644 index 3022d156d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_delete.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_dropdown.png b/app/src/main/res/drawable-xhdpi/voip_dropdown.png deleted file mode 100644 index d9fccac91..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_dropdown.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_earpiece.png b/app/src/main/res/drawable-xhdpi/voip_earpiece.png deleted file mode 100644 index e95a30801..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_earpiece.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_edit.png b/app/src/main/res/drawable-xhdpi/voip_edit.png deleted file mode 100644 index c24930212..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_edit.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_export.png b/app/src/main/res/drawable-xhdpi/voip_export.png deleted file mode 100644 index 3fdfa078a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_export.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_hangup.png b/app/src/main/res/drawable-xhdpi/voip_hangup.png deleted file mode 100644 index a2ceab5d8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_hangup.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_info.png b/app/src/main/res/drawable-xhdpi/voip_info.png deleted file mode 100644 index ae8dd86f8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_info.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_mandatory.png b/app/src/main/res/drawable-xhdpi/voip_mandatory.png deleted file mode 100644 index 4be37f7e4..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_mandatory.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_meeting_schedule.png b/app/src/main/res/drawable-xhdpi/voip_meeting_schedule.png deleted file mode 100644 index 168e3460f..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_meeting_schedule.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_menu_more.png b/app/src/main/res/drawable-xhdpi/voip_menu_more.png deleted file mode 100644 index 64abbbba8..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_menu_more.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_merge_calls.png b/app/src/main/res/drawable-xhdpi/voip_merge_calls.png deleted file mode 100644 index 6c4da5988..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_merge_calls.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_micro_off.png b/app/src/main/res/drawable-xhdpi/voip_micro_off.png deleted file mode 100644 index 57569b4f2..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_micro_off.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_micro_on.png b/app/src/main/res/drawable-xhdpi/voip_micro_on.png deleted file mode 100644 index 6552d35d5..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_micro_on.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_multiple_contacts_avatar.png b/app/src/main/res/drawable-xhdpi/voip_multiple_contacts_avatar.png deleted file mode 100644 index 5b0732545..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_multiple_contacts_avatar.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_multiple_contacts_avatar_alt.png b/app/src/main/res/drawable-xhdpi/voip_multiple_contacts_avatar_alt.png deleted file mode 100644 index 983c1448d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_multiple_contacts_avatar_alt.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_0.png b/app/src/main/res/drawable-xhdpi/voip_numpad_0.png deleted file mode 100644 index 115bdb17d..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_0.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_1.png b/app/src/main/res/drawable-xhdpi/voip_numpad_1.png deleted file mode 100644 index 4d8b7f5cc..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_1.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_2.png b/app/src/main/res/drawable-xhdpi/voip_numpad_2.png deleted file mode 100644 index 6b561c468..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_2.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_3.png b/app/src/main/res/drawable-xhdpi/voip_numpad_3.png deleted file mode 100644 index 386715586..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_3.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_4.png b/app/src/main/res/drawable-xhdpi/voip_numpad_4.png deleted file mode 100644 index e3dfdcc51..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_4.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_5.png b/app/src/main/res/drawable-xhdpi/voip_numpad_5.png deleted file mode 100644 index a18af28e5..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_5.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_6.png b/app/src/main/res/drawable-xhdpi/voip_numpad_6.png deleted file mode 100644 index 79279cb99..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_6.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_7.png b/app/src/main/res/drawable-xhdpi/voip_numpad_7.png deleted file mode 100644 index c68656fd3..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_7.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_8.png b/app/src/main/res/drawable-xhdpi/voip_numpad_8.png deleted file mode 100644 index 8d84c96ba..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_8.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_9.png b/app/src/main/res/drawable-xhdpi/voip_numpad_9.png deleted file mode 100644 index af3e0e0bf..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_9.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_hash.png b/app/src/main/res/drawable-xhdpi/voip_numpad_hash.png deleted file mode 100644 index 790e7d12e..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_hash.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_numpad_star.png b/app/src/main/res/drawable-xhdpi/voip_numpad_star.png deleted file mode 100644 index 5a2649de4..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_numpad_star.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_pause.png b/app/src/main/res/drawable-xhdpi/voip_pause.png deleted file mode 100644 index e888da937..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_pause.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_remote_recording.png b/app/src/main/res/drawable-xhdpi/voip_remote_recording.png deleted file mode 100644 index 3e4e94009..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_remote_recording.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_screenshot.png b/app/src/main/res/drawable-xhdpi/voip_screenshot.png deleted file mode 100644 index e1880264a..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_screenshot.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_single_contact_avatar.png b/app/src/main/res/drawable-xhdpi/voip_single_contact_avatar.png deleted file mode 100644 index e8806b0e4..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_single_contact_avatar.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_single_contact_avatar_alt.png b/app/src/main/res/drawable-xhdpi/voip_single_contact_avatar_alt.png deleted file mode 100644 index 58a69d43b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_single_contact_avatar_alt.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_speaker_off.png b/app/src/main/res/drawable-xhdpi/voip_speaker_off.png deleted file mode 100644 index c018c9899..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_speaker_off.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_speaker_on.png b/app/src/main/res/drawable-xhdpi/voip_speaker_on.png deleted file mode 100644 index 07f13c9b6..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_speaker_on.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/voip_spinner.png b/app/src/main/res/drawable-xhdpi/voip_spinner.png deleted file mode 100644 index 8238de56b..000000000 Binary files a/app/src/main/res/drawable-xhdpi/voip_spinner.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/topbar_call_notification.png b/app/src/main/res/drawable-xxhdpi/topbar_call_notification.png deleted file mode 100644 index ada072d3f..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/topbar_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/topbar_call_paused_notification.png b/app/src/main/res/drawable-xxhdpi/topbar_call_paused_notification.png deleted file mode 100644 index ef886c53c..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/topbar_call_paused_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/topbar_chat_notification.png b/app/src/main/res/drawable-xxhdpi/topbar_chat_notification.png deleted file mode 100644 index c00ce5b01..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/topbar_chat_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/topbar_missed_call_notification.png b/app/src/main/res/drawable-xxhdpi/topbar_missed_call_notification.png deleted file mode 100644 index c75fae050..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/topbar_missed_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/topbar_service_notification.png b/app/src/main/res/drawable-xxhdpi/topbar_service_notification.png deleted file mode 100644 index 89ab907d9..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/topbar_service_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/topbar_videocall_notification.png b/app/src/main/res/drawable-xxhdpi/topbar_videocall_notification.png deleted file mode 100644 index 1e5a1abb8..000000000 Binary files a/app/src/main/res/drawable-xxhdpi/topbar_videocall_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/topbar_call_notification.png b/app/src/main/res/drawable-xxxhdpi/topbar_call_notification.png deleted file mode 100644 index 130558d24..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/topbar_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/topbar_call_paused_notification.png b/app/src/main/res/drawable-xxxhdpi/topbar_call_paused_notification.png deleted file mode 100644 index 16c60ecdd..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/topbar_call_paused_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/topbar_chat_notification.png b/app/src/main/res/drawable-xxxhdpi/topbar_chat_notification.png deleted file mode 100644 index 144d25d0c..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/topbar_chat_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/topbar_missed_call_notification.png b/app/src/main/res/drawable-xxxhdpi/topbar_missed_call_notification.png deleted file mode 100644 index 1eb46e47b..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/topbar_missed_call_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/topbar_service_notification.png b/app/src/main/res/drawable-xxxhdpi/topbar_service_notification.png deleted file mode 100644 index bb12c773d..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/topbar_service_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/topbar_videocall_notification.png b/app/src/main/res/drawable-xxxhdpi/topbar_videocall_notification.png deleted file mode 100644 index 23184c99e..000000000 Binary files a/app/src/main/res/drawable-xxxhdpi/topbar_videocall_notification.png and /dev/null differ diff --git a/app/src/main/res/drawable/assistant_button.xml b/app/src/main/res/drawable/assistant_button.xml deleted file mode 100644 index 6dd7bfb0d..000000000 --- a/app/src/main/res/drawable/assistant_button.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/assistant_button_text_color.xml b/app/src/main/res/drawable/assistant_button_text_color.xml deleted file mode 100644 index 1797de9ce..000000000 --- a/app/src/main/res/drawable/assistant_button_text_color.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/avatar_border.xml b/app/src/main/res/drawable/avatar_border.xml deleted file mode 100644 index 045c7bfcb..000000000 --- a/app/src/main/res/drawable/avatar_border.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/back.xml b/app/src/main/res/drawable/back.xml deleted file mode 100644 index a298e171c..000000000 --- a/app/src/main/res/drawable/back.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/background_round_primary_color.xml b/app/src/main/res/drawable/background_round_primary_color.xml deleted file mode 100644 index 0907fcd1e..000000000 --- a/app/src/main/res/drawable/background_round_primary_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/background_round_secondary_color.xml b/app/src/main/res/drawable/background_round_secondary_color.xml deleted file mode 100644 index 4b8a04dad..000000000 --- a/app/src/main/res/drawable/background_round_secondary_color.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/backspace.xml b/app/src/main/res/drawable/backspace.xml deleted file mode 100644 index 70117222f..000000000 --- a/app/src/main/res/drawable/backspace.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/button_background.xml b/app/src/main/res/drawable/button_background.xml deleted file mode 100644 index 1b672fdfc..000000000 --- a/app/src/main/res/drawable/button_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/button_background_dark.xml b/app/src/main/res/drawable/button_background_dark.xml deleted file mode 100644 index 8f13c7bd7..000000000 --- a/app/src/main/res/drawable/button_background_dark.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/button_background_light.xml b/app/src/main/res/drawable/button_background_light.xml deleted file mode 100644 index c0df18fdd..000000000 --- a/app/src/main/res/drawable/button_background_light.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/button_background_reverse.xml b/app/src/main/res/drawable/button_background_reverse.xml deleted file mode 100644 index 9b7432f25..000000000 --- a/app/src/main/res/drawable/button_background_reverse.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/button_call_answer_background.xml b/app/src/main/res/drawable/button_call_answer_background.xml deleted file mode 100644 index e7f8c8abd..000000000 --- a/app/src/main/res/drawable/button_call_answer_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/button_call_context_menu_background.xml b/app/src/main/res/drawable/button_call_context_menu_background.xml deleted file mode 100644 index c20f7aa56..000000000 --- a/app/src/main/res/drawable/button_call_context_menu_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/button_call_numpad_background.xml b/app/src/main/res/drawable/button_call_numpad_background.xml deleted file mode 100644 index b30990ebf..000000000 --- a/app/src/main/res/drawable/button_call_numpad_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/button_call_recording_background.xml b/app/src/main/res/drawable/button_call_recording_background.xml deleted file mode 100644 index 1f3680b7c..000000000 --- a/app/src/main/res/drawable/button_call_recording_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/button_call_terminate_background.xml b/app/src/main/res/drawable/button_call_terminate_background.xml deleted file mode 100644 index f584585fe..000000000 --- a/app/src/main/res/drawable/button_call_terminate_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/button_conference_info.xml b/app/src/main/res/drawable/button_conference_info.xml deleted file mode 100644 index d60f4e6fa..000000000 --- a/app/src/main/res/drawable/button_conference_info.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/button_green_background.xml b/app/src/main/res/drawable/button_green_background.xml deleted file mode 100644 index 8d4cfbb68..000000000 --- a/app/src/main/res/drawable/button_green_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/button_orange_background.xml b/app/src/main/res/drawable/button_orange_background.xml deleted file mode 100644 index bff93f486..000000000 --- a/app/src/main/res/drawable/button_orange_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/button_round_background.xml b/app/src/main/res/drawable/button_round_background.xml deleted file mode 100644 index afc56ca3b..000000000 --- a/app/src/main/res/drawable/button_round_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/button_toggle_background.xml b/app/src/main/res/drawable/button_toggle_background.xml deleted file mode 100644 index c2292dae4..000000000 --- a/app/src/main/res/drawable/button_toggle_background.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/button_toggle_background_reverse.xml b/app/src/main/res/drawable/button_toggle_background_reverse.xml deleted file mode 100644 index 3ef75d708..000000000 --- a/app/src/main/res/drawable/button_toggle_background_reverse.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/call.xml b/app/src/main/res/drawable/call.xml deleted file mode 100644 index 9ed0f637e..000000000 --- a/app/src/main/res/drawable/call.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/call_alt_start.xml b/app/src/main/res/drawable/call_alt_start.xml deleted file mode 100644 index 43d9d8be7..000000000 --- a/app/src/main/res/drawable/call_alt_start.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/call_back.xml b/app/src/main/res/drawable/call_back.xml deleted file mode 100644 index 1442cb11c..000000000 --- a/app/src/main/res/drawable/call_back.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/camera_switch.xml b/app/src/main/res/drawable/camera_switch.xml deleted file mode 100644 index a79f649b7..000000000 --- a/app/src/main/res/drawable/camera_switch.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/cancel.xml b/app/src/main/res/drawable/cancel.xml deleted file mode 100644 index 67bd03f05..000000000 --- a/app/src/main/res/drawable/cancel.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/cancel_shape.xml b/app/src/main/res/drawable/cancel_shape.xml deleted file mode 100644 index fef6bb93a..000000000 --- a/app/src/main/res/drawable/cancel_shape.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_bubble_incoming_full.xml b/app/src/main/res/drawable/chat_bubble_incoming_full.xml deleted file mode 100644 index a8d9a3b32..000000000 --- a/app/src/main/res/drawable/chat_bubble_incoming_full.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_incoming_reactions.xml b/app/src/main/res/drawable/chat_bubble_incoming_reactions.xml deleted file mode 100644 index 31e85ee7a..000000000 --- a/app/src/main/res/drawable/chat_bubble_incoming_reactions.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_incoming_split_1.xml b/app/src/main/res/drawable/chat_bubble_incoming_split_1.xml deleted file mode 100644 index d531a9917..000000000 --- a/app/src/main/res/drawable/chat_bubble_incoming_split_1.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_incoming_split_2.xml b/app/src/main/res/drawable/chat_bubble_incoming_split_2.xml deleted file mode 100644 index 277b01a2f..000000000 --- a/app/src/main/res/drawable/chat_bubble_incoming_split_2.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_incoming_split_3.xml b/app/src/main/res/drawable/chat_bubble_incoming_split_3.xml deleted file mode 100644 index 7bc997138..000000000 --- a/app/src/main/res/drawable/chat_bubble_incoming_split_3.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_full.xml b/app/src/main/res/drawable/chat_bubble_outgoing_full.xml deleted file mode 100644 index 2117a9a5e..000000000 --- a/app/src/main/res/drawable/chat_bubble_outgoing_full.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_reactions.xml b/app/src/main/res/drawable/chat_bubble_outgoing_reactions.xml deleted file mode 100644 index d06e116ae..000000000 --- a/app/src/main/res/drawable/chat_bubble_outgoing_reactions.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_split_1.xml b/app/src/main/res/drawable/chat_bubble_outgoing_split_1.xml deleted file mode 100644 index 42f873e03..000000000 --- a/app/src/main/res/drawable/chat_bubble_outgoing_split_1.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_split_2.xml b/app/src/main/res/drawable/chat_bubble_outgoing_split_2.xml deleted file mode 100644 index 97bd2976e..000000000 --- a/app/src/main/res/drawable/chat_bubble_outgoing_split_2.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_outgoing_split_3.xml b/app/src/main/res/drawable/chat_bubble_outgoing_split_3.xml deleted file mode 100644 index e1c6f9d03..000000000 --- a/app/src/main/res/drawable/chat_bubble_outgoing_split_3.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_reply_background.xml b/app/src/main/res/drawable/chat_bubble_reply_background.xml deleted file mode 100644 index 3725abfdf..000000000 --- a/app/src/main/res/drawable/chat_bubble_reply_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_reply_file_background.xml b/app/src/main/res/drawable/chat_bubble_reply_file_background.xml deleted file mode 100644 index 5b2b91dca..000000000 --- a/app/src/main/res/drawable/chat_bubble_reply_file_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_reply_incoming_indicator.xml b/app/src/main/res/drawable/chat_bubble_reply_incoming_indicator.xml deleted file mode 100644 index 3bb03b37b..000000000 --- a/app/src/main/res/drawable/chat_bubble_reply_incoming_indicator.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_bubble_reply_outgoing_indicator.xml b/app/src/main/res/drawable/chat_bubble_reply_outgoing_indicator.xml deleted file mode 100644 index 3cd2b9b0b..000000000 --- a/app/src/main/res/drawable/chat_bubble_reply_outgoing_indicator.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/chat_file.xml b/app/src/main/res/drawable/chat_file.xml deleted file mode 100644 index 5a820aa57..000000000 --- a/app/src/main/res/drawable/chat_file.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/chat_group_add.xml b/app/src/main/res/drawable/chat_group_add.xml deleted file mode 100644 index 9ee4744df..000000000 --- a/app/src/main/res/drawable/chat_group_add.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/chat_group_new.xml b/app/src/main/res/drawable/chat_group_new.xml deleted file mode 100644 index 16fac5aa7..000000000 --- a/app/src/main/res/drawable/chat_group_new.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/chat_message_audio_record_preview_progress.xml b/app/src/main/res/drawable/chat_message_audio_record_preview_progress.xml deleted file mode 100644 index f6419f6e9..000000000 --- a/app/src/main/res/drawable/chat_message_audio_record_preview_progress.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_message_audio_record_progress.xml b/app/src/main/res/drawable/chat_message_audio_record_progress.xml deleted file mode 100644 index c004038dc..000000000 --- a/app/src/main/res/drawable/chat_message_audio_record_progress.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/chat_message_voice_recording_background.xml b/app/src/main/res/drawable/chat_message_voice_recording_background.xml deleted file mode 100644 index 3725abfdf..000000000 --- a/app/src/main/res/drawable/chat_message_voice_recording_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_new.xml b/app/src/main/res/drawable/chat_new.xml deleted file mode 100644 index fa5e61254..000000000 --- a/app/src/main/res/drawable/chat_new.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_add_contact.xml b/app/src/main/res/drawable/chat_room_menu_add_contact.xml deleted file mode 100644 index 474e025f6..000000000 --- a/app/src/main/res/drawable/chat_room_menu_add_contact.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_delete.xml b/app/src/main/res/drawable/chat_room_menu_delete.xml deleted file mode 100644 index cb9f81675..000000000 --- a/app/src/main/res/drawable/chat_room_menu_delete.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_devices.xml b/app/src/main/res/drawable/chat_room_menu_devices.xml deleted file mode 100644 index f4453ccda..000000000 --- a/app/src/main/res/drawable/chat_room_menu_devices.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_ephemeral.xml b/app/src/main/res/drawable/chat_room_menu_ephemeral.xml deleted file mode 100644 index 55433de52..000000000 --- a/app/src/main/res/drawable/chat_room_menu_ephemeral.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_group_info.xml b/app/src/main/res/drawable/chat_room_menu_group_info.xml deleted file mode 100644 index d15e39140..000000000 --- a/app/src/main/res/drawable/chat_room_menu_group_info.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_meeting.xml b/app/src/main/res/drawable/chat_room_menu_meeting.xml deleted file mode 100644 index 88f9dfd62..000000000 --- a/app/src/main/res/drawable/chat_room_menu_meeting.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_mute_notifications.xml b/app/src/main/res/drawable/chat_room_menu_mute_notifications.xml deleted file mode 100644 index 34f6f8794..000000000 --- a/app/src/main/res/drawable/chat_room_menu_mute_notifications.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_unmute_notifications.xml b/app/src/main/res/drawable/chat_room_menu_unmute_notifications.xml deleted file mode 100644 index e53b378eb..000000000 --- a/app/src/main/res/drawable/chat_room_menu_unmute_notifications.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_room_menu_view_contact.xml b/app/src/main/res/drawable/chat_room_menu_view_contact.xml deleted file mode 100644 index 248584507..000000000 --- a/app/src/main/res/drawable/chat_room_menu_view_contact.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/chat_send_message.xml b/app/src/main/res/drawable/chat_send_message.xml deleted file mode 100644 index 47af131b5..000000000 --- a/app/src/main/res/drawable/chat_send_message.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/chevron_list_close.xml b/app/src/main/res/drawable/chevron_list_close.xml deleted file mode 100644 index d826e42e9..000000000 --- a/app/src/main/res/drawable/chevron_list_close.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/chevron_list_open.xml b/app/src/main/res/drawable/chevron_list_open.xml deleted file mode 100644 index 37bcf729b..000000000 --- a/app/src/main/res/drawable/chevron_list_open.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/contact.xml b/app/src/main/res/drawable/contact.xml deleted file mode 100644 index 3a33fc108..000000000 --- a/app/src/main/res/drawable/contact.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/contact_add.xml b/app/src/main/res/drawable/contact_add.xml deleted file mode 100644 index d3e79717c..000000000 --- a/app/src/main/res/drawable/contact_add.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/contacts_all.xml b/app/src/main/res/drawable/contacts_all.xml deleted file mode 100644 index b6ecf35ff..000000000 --- a/app/src/main/res/drawable/contacts_all.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/contacts_sip.xml b/app/src/main/res/drawable/contacts_sip.xml deleted file mode 100644 index 33d9524f0..000000000 --- a/app/src/main/res/drawable/contacts_sip.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/delete.xml b/app/src/main/res/drawable/delete.xml deleted file mode 100644 index a3000e1f3..000000000 --- a/app/src/main/res/drawable/delete.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/deselect_all.xml b/app/src/main/res/drawable/deselect_all.xml deleted file mode 100644 index 2011e9ade..000000000 --- a/app/src/main/res/drawable/deselect_all.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/dialog_delete_icon.xml b/app/src/main/res/drawable/dialog_delete_icon.xml deleted file mode 100644 index bf1d1d574..000000000 --- a/app/src/main/res/drawable/dialog_delete_icon.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/divider.xml b/app/src/main/res/drawable/divider.xml deleted file mode 100644 index 1ac7d6320..000000000 --- a/app/src/main/res/drawable/divider.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/edit.xml b/app/src/main/res/drawable/edit.xml deleted file mode 100644 index 180eea212..000000000 --- a/app/src/main/res/drawable/edit.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/emoji.xml b/app/src/main/res/drawable/emoji.xml deleted file mode 100644 index 3785640c2..000000000 --- a/app/src/main/res/drawable/emoji.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ephemeral_messages.xml b/app/src/main/res/drawable/ephemeral_messages.xml deleted file mode 100644 index 3b8fb391d..000000000 --- a/app/src/main/res/drawable/ephemeral_messages.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/event_decoration_gray.xml b/app/src/main/res/drawable/event_decoration_gray.xml deleted file mode 100644 index 626663ad7..000000000 --- a/app/src/main/res/drawable/event_decoration_gray.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/event_decoration_red.xml b/app/src/main/res/drawable/event_decoration_red.xml deleted file mode 100644 index f9e4927bf..000000000 --- a/app/src/main/res/drawable/event_decoration_red.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_add.xml b/app/src/main/res/drawable/field_add.xml deleted file mode 100644 index d5ab69493..000000000 --- a/app/src/main/res/drawable/field_add.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/field_add_default.xml b/app/src/main/res/drawable/field_add_default.xml deleted file mode 100644 index 29cf8f588..000000000 --- a/app/src/main/res/drawable/field_add_default.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_add_disabled.xml b/app/src/main/res/drawable/field_add_disabled.xml deleted file mode 100644 index 4421590c7..000000000 --- a/app/src/main/res/drawable/field_add_disabled.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_add_over.xml b/app/src/main/res/drawable/field_add_over.xml deleted file mode 100644 index 496327588..000000000 --- a/app/src/main/res/drawable/field_add_over.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_add_shape.xml b/app/src/main/res/drawable/field_add_shape.xml deleted file mode 100644 index a335e3ba3..000000000 --- a/app/src/main/res/drawable/field_add_shape.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_clean.xml b/app/src/main/res/drawable/field_clean.xml deleted file mode 100644 index 0400c9278..000000000 --- a/app/src/main/res/drawable/field_clean.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/field_clean_default.xml b/app/src/main/res/drawable/field_clean_default.xml deleted file mode 100644 index b29f380c1..000000000 --- a/app/src/main/res/drawable/field_clean_default.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_clean_over.xml b/app/src/main/res/drawable/field_clean_over.xml deleted file mode 100644 index a5c707556..000000000 --- a/app/src/main/res/drawable/field_clean_over.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_clean_shape.xml b/app/src/main/res/drawable/field_clean_shape.xml deleted file mode 100644 index 1c9e1349b..000000000 --- a/app/src/main/res/drawable/field_clean_shape.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_remove.xml b/app/src/main/res/drawable/field_remove.xml deleted file mode 100644 index 3fc616dd3..000000000 --- a/app/src/main/res/drawable/field_remove.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/field_remove_default.xml b/app/src/main/res/drawable/field_remove_default.xml deleted file mode 100644 index 214b742f1..000000000 --- a/app/src/main/res/drawable/field_remove_default.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_remove_disabled.xml b/app/src/main/res/drawable/field_remove_disabled.xml deleted file mode 100644 index 5c582da40..000000000 --- a/app/src/main/res/drawable/field_remove_disabled.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_remove_over.xml b/app/src/main/res/drawable/field_remove_over.xml deleted file mode 100644 index b8a988b7d..000000000 --- a/app/src/main/res/drawable/field_remove_over.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/field_remove_shape.xml b/app/src/main/res/drawable/field_remove_shape.xml deleted file mode 100644 index 75f5c4f69..000000000 --- a/app/src/main/res/drawable/field_remove_shape.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/file.xml b/app/src/main/res/drawable/file.xml deleted file mode 100644 index f874e16f6..000000000 --- a/app/src/main/res/drawable/file.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/file_audio.xml b/app/src/main/res/drawable/file_audio.xml deleted file mode 100644 index 2adba2380..000000000 --- a/app/src/main/res/drawable/file_audio.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/file_pdf.xml b/app/src/main/res/drawable/file_pdf.xml deleted file mode 100644 index a932850e1..000000000 --- a/app/src/main/res/drawable/file_pdf.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/file_picture.xml b/app/src/main/res/drawable/file_picture.xml deleted file mode 100644 index 6f49ac200..000000000 --- a/app/src/main/res/drawable/file_picture.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/file_video.xml b/app/src/main/res/drawable/file_video.xml deleted file mode 100644 index 1b178b560..000000000 --- a/app/src/main/res/drawable/file_video.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/footer_button.xml b/app/src/main/res/drawable/footer_button.xml deleted file mode 100644 index b23178869..000000000 --- a/app/src/main/res/drawable/footer_button.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/generated_avatar_bg.xml b/app/src/main/res/drawable/generated_avatar_bg.xml deleted file mode 100644 index 87e6684b1..000000000 --- a/app/src/main/res/drawable/generated_avatar_bg.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/hidden_unread_message_count_bg.xml b/app/src/main/res/drawable/hidden_unread_message_count_bg.xml deleted file mode 100644 index 6c1145f56..000000000 --- a/app/src/main/res/drawable/hidden_unread_message_count_bg.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/history_all.xml b/app/src/main/res/drawable/history_all.xml deleted file mode 100644 index b3f33e5c1..000000000 --- a/app/src/main/res/drawable/history_all.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/history_detail_background.xml b/app/src/main/res/drawable/history_detail_background.xml deleted file mode 100644 index c1c0dbff4..000000000 --- a/app/src/main/res/drawable/history_detail_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/history_detail_background_pressed.xml b/app/src/main/res/drawable/history_detail_background_pressed.xml deleted file mode 100644 index 50f07b529..000000000 --- a/app/src/main/res/drawable/history_detail_background_pressed.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/history_missed.xml b/app/src/main/res/drawable/history_missed.xml deleted file mode 100644 index 4a634f2ca..000000000 --- a/app/src/main/res/drawable/history_missed.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_apply.xml b/app/src/main/res/drawable/icon_apply.xml deleted file mode 100644 index 3be5be72e..000000000 --- a/app/src/main/res/drawable/icon_apply.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_audio_routes.xml b/app/src/main/res/drawable/icon_audio_routes.xml deleted file mode 100644 index b694e1b51..000000000 --- a/app/src/main/res/drawable/icon_audio_routes.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_bluetooth.xml b/app/src/main/res/drawable/icon_bluetooth.xml deleted file mode 100644 index 42ad75065..000000000 --- a/app/src/main/res/drawable/icon_bluetooth.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_call_add.xml b/app/src/main/res/drawable/icon_call_add.xml deleted file mode 100644 index f559fbf98..000000000 --- a/app/src/main/res/drawable/icon_call_add.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_call_answer.xml b/app/src/main/res/drawable/icon_call_answer.xml deleted file mode 100644 index 86a06a63b..000000000 --- a/app/src/main/res/drawable/icon_call_answer.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_call_answer_video.xml b/app/src/main/res/drawable/icon_call_answer_video.xml deleted file mode 100644 index 849f6050e..000000000 --- a/app/src/main/res/drawable/icon_call_answer_video.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_call_camera_switch.xml b/app/src/main/res/drawable/icon_call_camera_switch.xml deleted file mode 100644 index d8b0d6de6..000000000 --- a/app/src/main/res/drawable/icon_call_camera_switch.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_call_chat.xml b/app/src/main/res/drawable/icon_call_chat.xml deleted file mode 100644 index ad97ebc4f..000000000 --- a/app/src/main/res/drawable/icon_call_chat.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_call_chat_rooms.xml b/app/src/main/res/drawable/icon_call_chat_rooms.xml deleted file mode 100644 index 584556375..000000000 --- a/app/src/main/res/drawable/icon_call_chat_rooms.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_call_forward.xml b/app/src/main/res/drawable/icon_call_forward.xml deleted file mode 100644 index 7caa899f1..000000000 --- a/app/src/main/res/drawable/icon_call_forward.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_call_hangup.xml b/app/src/main/res/drawable/icon_call_hangup.xml deleted file mode 100644 index 35cc67534..000000000 --- a/app/src/main/res/drawable/icon_call_hangup.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_call_more.xml b/app/src/main/res/drawable/icon_call_more.xml deleted file mode 100644 index 488e9e3bf..000000000 --- a/app/src/main/res/drawable/icon_call_more.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_call_numpad.xml b/app/src/main/res/drawable/icon_call_numpad.xml deleted file mode 100644 index 6768b101a..000000000 --- a/app/src/main/res/drawable/icon_call_numpad.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_call_participants.xml b/app/src/main/res/drawable/icon_call_participants.xml deleted file mode 100644 index 31f960464..000000000 --- a/app/src/main/res/drawable/icon_call_participants.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_call_record.xml b/app/src/main/res/drawable/icon_call_record.xml deleted file mode 100644 index 2d3fdbdf9..000000000 --- a/app/src/main/res/drawable/icon_call_record.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_call_screenshot.xml b/app/src/main/res/drawable/icon_call_screenshot.xml deleted file mode 100644 index 6fe24f800..000000000 --- a/app/src/main/res/drawable/icon_call_screenshot.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_call_stats.xml b/app/src/main/res/drawable/icon_call_stats.xml deleted file mode 100644 index 3cf57a966..000000000 --- a/app/src/main/res/drawable/icon_call_stats.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_calls_list.xml b/app/src/main/res/drawable/icon_calls_list.xml deleted file mode 100644 index 937a6ff20..000000000 --- a/app/src/main/res/drawable/icon_calls_list.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_calls_list_menu.xml b/app/src/main/res/drawable/icon_calls_list_menu.xml deleted file mode 100644 index 3a4602cee..000000000 --- a/app/src/main/res/drawable/icon_calls_list_menu.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_cancel.xml b/app/src/main/res/drawable/icon_cancel.xml deleted file mode 100644 index 052b79e93..000000000 --- a/app/src/main/res/drawable/icon_cancel.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_cancel_alt.xml b/app/src/main/res/drawable/icon_cancel_alt.xml deleted file mode 100644 index 07d6cc058..000000000 --- a/app/src/main/res/drawable/icon_cancel_alt.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_conference_layout_active_speaker.xml b/app/src/main/res/drawable/icon_conference_layout_active_speaker.xml deleted file mode 100644 index eb53c13ea..000000000 --- a/app/src/main/res/drawable/icon_conference_layout_active_speaker.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_conference_layout_audio_only.xml b/app/src/main/res/drawable/icon_conference_layout_audio_only.xml deleted file mode 100644 index 3736e6500..000000000 --- a/app/src/main/res/drawable/icon_conference_layout_audio_only.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_conference_layout_grid.xml b/app/src/main/res/drawable/icon_conference_layout_grid.xml deleted file mode 100644 index 1f3bbc190..000000000 --- a/app/src/main/res/drawable/icon_conference_layout_grid.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_copy.xml b/app/src/main/res/drawable/icon_copy.xml deleted file mode 100644 index 278fa0562..000000000 --- a/app/src/main/res/drawable/icon_copy.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_delete.xml b/app/src/main/res/drawable/icon_delete.xml deleted file mode 100644 index bd647fa54..000000000 --- a/app/src/main/res/drawable/icon_delete.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_earpiece.xml b/app/src/main/res/drawable/icon_earpiece.xml deleted file mode 100644 index 69b03ecc0..000000000 --- a/app/src/main/res/drawable/icon_earpiece.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_edit.xml b/app/src/main/res/drawable/icon_edit.xml deleted file mode 100644 index d9a3f921a..000000000 --- a/app/src/main/res/drawable/icon_edit.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_edit_alt.xml b/app/src/main/res/drawable/icon_edit_alt.xml deleted file mode 100644 index 74da8157b..000000000 --- a/app/src/main/res/drawable/icon_edit_alt.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_ephemeral_message.xml b/app/src/main/res/drawable/icon_ephemeral_message.xml deleted file mode 100644 index 299072bab..000000000 --- a/app/src/main/res/drawable/icon_ephemeral_message.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_forwarded_message.xml b/app/src/main/res/drawable/icon_forwarded_message.xml deleted file mode 100644 index c210b3dce..000000000 --- a/app/src/main/res/drawable/icon_forwarded_message.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_info.xml b/app/src/main/res/drawable/icon_info.xml deleted file mode 100644 index 99fd6a38f..000000000 --- a/app/src/main/res/drawable/icon_info.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_info_selected.xml b/app/src/main/res/drawable/icon_info_selected.xml deleted file mode 100644 index 67b628ef2..000000000 --- a/app/src/main/res/drawable/icon_info_selected.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_meeting_schedule.xml b/app/src/main/res/drawable/icon_meeting_schedule.xml deleted file mode 100644 index 9fcd02f66..000000000 --- a/app/src/main/res/drawable/icon_meeting_schedule.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_menu_more.xml b/app/src/main/res/drawable/icon_menu_more.xml deleted file mode 100644 index 19c7e24b7..000000000 --- a/app/src/main/res/drawable/icon_menu_more.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_merge_calls_local_conference.xml b/app/src/main/res/drawable/icon_merge_calls_local_conference.xml deleted file mode 100644 index 2fdd2e68a..000000000 --- a/app/src/main/res/drawable/icon_merge_calls_local_conference.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_mic_muted.xml b/app/src/main/res/drawable/icon_mic_muted.xml deleted file mode 100644 index 1ab7fc636..000000000 --- a/app/src/main/res/drawable/icon_mic_muted.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_multiple_contacts_avatar.xml b/app/src/main/res/drawable/icon_multiple_contacts_avatar.xml deleted file mode 100644 index 15fac96a6..000000000 --- a/app/src/main/res/drawable/icon_multiple_contacts_avatar.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_next.xml b/app/src/main/res/drawable/icon_next.xml deleted file mode 100644 index 968192312..000000000 --- a/app/src/main/res/drawable/icon_next.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_pause.xml b/app/src/main/res/drawable/icon_pause.xml deleted file mode 100644 index 09787beba..000000000 --- a/app/src/main/res/drawable/icon_pause.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_replied_message.xml b/app/src/main/res/drawable/icon_replied_message.xml deleted file mode 100644 index dcc78b5c6..000000000 --- a/app/src/main/res/drawable/icon_replied_message.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_schedule_date.xml b/app/src/main/res/drawable/icon_schedule_date.xml deleted file mode 100644 index 5f29ac622..000000000 --- a/app/src/main/res/drawable/icon_schedule_date.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_schedule_participants.xml b/app/src/main/res/drawable/icon_schedule_participants.xml deleted file mode 100644 index 45741792a..000000000 --- a/app/src/main/res/drawable/icon_schedule_participants.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_schedule_time.xml b/app/src/main/res/drawable/icon_schedule_time.xml deleted file mode 100644 index f165174c6..000000000 --- a/app/src/main/res/drawable/icon_schedule_time.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_share.xml b/app/src/main/res/drawable/icon_share.xml deleted file mode 100644 index 4b7a252ee..000000000 --- a/app/src/main/res/drawable/icon_share.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/drawable/icon_single_contact_avatar.xml b/app/src/main/res/drawable/icon_single_contact_avatar.xml deleted file mode 100644 index 0af513c72..000000000 --- a/app/src/main/res/drawable/icon_single_contact_avatar.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_speaker.xml b/app/src/main/res/drawable/icon_speaker.xml deleted file mode 100644 index ab3fd031b..000000000 --- a/app/src/main/res/drawable/icon_speaker.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_spinner.xml b/app/src/main/res/drawable/icon_spinner.xml deleted file mode 100644 index f3537e7bd..000000000 --- a/app/src/main/res/drawable/icon_spinner.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_spinner_background.xml b/app/src/main/res/drawable/icon_spinner_background.xml deleted file mode 100644 index f50b58b1b..000000000 --- a/app/src/main/res/drawable/icon_spinner_background.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_spinner_rotating.xml b/app/src/main/res/drawable/icon_spinner_rotating.xml deleted file mode 100644 index 6b8ed48ca..000000000 --- a/app/src/main/res/drawable/icon_spinner_rotating.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/icon_toggle_camera.xml b/app/src/main/res/drawable/icon_toggle_camera.xml deleted file mode 100644 index 80a10a84f..000000000 --- a/app/src/main/res/drawable/icon_toggle_camera.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_toggle_mic.xml b/app/src/main/res/drawable/icon_toggle_mic.xml deleted file mode 100644 index 40b18e3f8..000000000 --- a/app/src/main/res/drawable/icon_toggle_mic.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_toggle_speaker.xml b/app/src/main/res/drawable/icon_toggle_speaker.xml deleted file mode 100644 index e9f43e1a1..000000000 --- a/app/src/main/res/drawable/icon_toggle_speaker.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_video_conf_history_filter.xml b/app/src/main/res/drawable/icon_video_conf_history_filter.xml deleted file mode 100644 index 65b3aa6ed..000000000 --- a/app/src/main/res/drawable/icon_video_conf_history_filter.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/icon_video_conf_incoming.xml b/app/src/main/res/drawable/icon_video_conf_incoming.xml deleted file mode 100644 index 57bca35d7..000000000 --- a/app/src/main/res/drawable/icon_video_conf_incoming.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/icon_video_conf_new.xml b/app/src/main/res/drawable/icon_video_conf_new.xml deleted file mode 100644 index 2c1dd4e58..000000000 --- a/app/src/main/res/drawable/icon_video_conf_new.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/led_away.xml b/app/src/main/res/drawable/led_away.xml deleted file mode 100644 index 53b2810ea..000000000 --- a/app/src/main/res/drawable/led_away.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/led_background.xml b/app/src/main/res/drawable/led_background.xml deleted file mode 100644 index d46d2c20a..000000000 --- a/app/src/main/res/drawable/led_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/led_do_not_disturb.xml b/app/src/main/res/drawable/led_do_not_disturb.xml deleted file mode 100644 index e2ce8ac4b..000000000 --- a/app/src/main/res/drawable/led_do_not_disturb.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/led_error.xml b/app/src/main/res/drawable/led_error.xml deleted file mode 100644 index e2ce8ac4b..000000000 --- a/app/src/main/res/drawable/led_error.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/led_not_registered.xml b/app/src/main/res/drawable/led_not_registered.xml deleted file mode 100644 index f95c698ac..000000000 --- a/app/src/main/res/drawable/led_not_registered.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/led_online.xml b/app/src/main/res/drawable/led_online.xml deleted file mode 100644 index 7c701e958..000000000 --- a/app/src/main/res/drawable/led_online.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/led_registered.xml b/app/src/main/res/drawable/led_registered.xml deleted file mode 100644 index 7c701e958..000000000 --- a/app/src/main/res/drawable/led_registered.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/led_registration_in_progress.xml b/app/src/main/res/drawable/led_registration_in_progress.xml deleted file mode 100644 index 53b2810ea..000000000 --- a/app/src/main/res/drawable/led_registration_in_progress.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/linphone_logo_tinted.xml b/app/src/main/res/drawable/linphone_logo_tinted.xml deleted file mode 100644 index e04a976a4..000000000 --- a/app/src/main/res/drawable/linphone_logo_tinted.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/list_detail.xml b/app/src/main/res/drawable/list_detail.xml deleted file mode 100644 index 271ded33e..000000000 --- a/app/src/main/res/drawable/list_detail.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/menu.xml b/app/src/main/res/drawable/menu.xml deleted file mode 100644 index 035606757..000000000 --- a/app/src/main/res/drawable/menu.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/menu_about.xml b/app/src/main/res/drawable/menu_about.xml deleted file mode 100644 index 9fbb4ec59..000000000 --- a/app/src/main/res/drawable/menu_about.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/menu_assistant.xml b/app/src/main/res/drawable/menu_assistant.xml deleted file mode 100644 index 44b13bd10..000000000 --- a/app/src/main/res/drawable/menu_assistant.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/menu_background.xml b/app/src/main/res/drawable/menu_background.xml deleted file mode 100644 index 414e4d2eb..000000000 --- a/app/src/main/res/drawable/menu_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/menu_background_color.xml b/app/src/main/res/drawable/menu_background_color.xml deleted file mode 100644 index b0f1a4a64..000000000 --- a/app/src/main/res/drawable/menu_background_color.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/menu_copy_text.xml b/app/src/main/res/drawable/menu_copy_text.xml deleted file mode 100644 index 1e3a90666..000000000 --- a/app/src/main/res/drawable/menu_copy_text.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/menu_delete.xml b/app/src/main/res/drawable/menu_delete.xml deleted file mode 100644 index 95753c8e2..000000000 --- a/app/src/main/res/drawable/menu_delete.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/menu_forward.xml b/app/src/main/res/drawable/menu_forward.xml deleted file mode 100644 index 15c32f263..000000000 --- a/app/src/main/res/drawable/menu_forward.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/menu_imdn_info.xml b/app/src/main/res/drawable/menu_imdn_info.xml deleted file mode 100644 index d15e39140..000000000 --- a/app/src/main/res/drawable/menu_imdn_info.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/menu_options.xml b/app/src/main/res/drawable/menu_options.xml deleted file mode 100644 index 056ff36da..000000000 --- a/app/src/main/res/drawable/menu_options.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/menu_pressed_background_color.xml b/app/src/main/res/drawable/menu_pressed_background_color.xml deleted file mode 100644 index ae56471dd..000000000 --- a/app/src/main/res/drawable/menu_pressed_background_color.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/menu_recordings.xml b/app/src/main/res/drawable/menu_recordings.xml deleted file mode 100644 index e4d5f5e5e..000000000 --- a/app/src/main/res/drawable/menu_recordings.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/menu_reply.xml b/app/src/main/res/drawable/menu_reply.xml deleted file mode 100644 index c3099128f..000000000 --- a/app/src/main/res/drawable/menu_reply.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/numpad_eight.xml b/app/src/main/res/drawable/numpad_eight.xml deleted file mode 100644 index eba2595e3..000000000 --- a/app/src/main/res/drawable/numpad_eight.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_five.xml b/app/src/main/res/drawable/numpad_five.xml deleted file mode 100644 index bd19a35a0..000000000 --- a/app/src/main/res/drawable/numpad_five.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_four.xml b/app/src/main/res/drawable/numpad_four.xml deleted file mode 100644 index 791010381..000000000 --- a/app/src/main/res/drawable/numpad_four.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_nine.xml b/app/src/main/res/drawable/numpad_nine.xml deleted file mode 100644 index 4fcf8ba72..000000000 --- a/app/src/main/res/drawable/numpad_nine.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_one.xml b/app/src/main/res/drawable/numpad_one.xml deleted file mode 100644 index 392b53e3f..000000000 --- a/app/src/main/res/drawable/numpad_one.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_seven.xml b/app/src/main/res/drawable/numpad_seven.xml deleted file mode 100644 index a8aad2d4f..000000000 --- a/app/src/main/res/drawable/numpad_seven.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_sharp.xml b/app/src/main/res/drawable/numpad_sharp.xml deleted file mode 100644 index 84bbed502..000000000 --- a/app/src/main/res/drawable/numpad_sharp.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_six.xml b/app/src/main/res/drawable/numpad_six.xml deleted file mode 100644 index de101da62..000000000 --- a/app/src/main/res/drawable/numpad_six.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_star_digit.xml b/app/src/main/res/drawable/numpad_star_digit.xml deleted file mode 100644 index 985778188..000000000 --- a/app/src/main/res/drawable/numpad_star_digit.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_three.xml b/app/src/main/res/drawable/numpad_three.xml deleted file mode 100644 index ed428bb70..000000000 --- a/app/src/main/res/drawable/numpad_three.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_two.xml b/app/src/main/res/drawable/numpad_two.xml deleted file mode 100644 index 3762419f4..000000000 --- a/app/src/main/res/drawable/numpad_two.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/numpad_zero.xml b/app/src/main/res/drawable/numpad_zero.xml deleted file mode 100644 index c43fe66e8..000000000 --- a/app/src/main/res/drawable/numpad_zero.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/record_audio_message.xml b/app/src/main/res/drawable/record_audio_message.xml deleted file mode 100644 index a3b70050b..000000000 --- a/app/src/main/res/drawable/record_audio_message.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/record_pause_dark.xml b/app/src/main/res/drawable/record_pause_dark.xml deleted file mode 100644 index 7e7a59878..000000000 --- a/app/src/main/res/drawable/record_pause_dark.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/record_pause_light.xml b/app/src/main/res/drawable/record_pause_light.xml deleted file mode 100644 index 5af5e7552..000000000 --- a/app/src/main/res/drawable/record_pause_light.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/record_play_dark.xml b/app/src/main/res/drawable/record_play_dark.xml deleted file mode 100644 index 33721dc80..000000000 --- a/app/src/main/res/drawable/record_play_dark.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/record_play_light.xml b/app/src/main/res/drawable/record_play_light.xml deleted file mode 100644 index f17480c37..000000000 --- a/app/src/main/res/drawable/record_play_light.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/record_stop_light.xml b/app/src/main/res/drawable/record_stop_light.xml deleted file mode 100644 index 1d80da938..000000000 --- a/app/src/main/res/drawable/record_stop_light.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/recording_play_pause.xml b/app/src/main/res/drawable/recording_play_pause.xml deleted file mode 100644 index cb4e8130a..000000000 --- a/app/src/main/res/drawable/recording_play_pause.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - diff --git a/app/src/main/res/drawable/rect_orange_button.xml b/app/src/main/res/drawable/rect_orange_button.xml deleted file mode 100644 index 683e0fb36..000000000 --- a/app/src/main/res/drawable/rect_orange_button.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/resizable_assistant_button.xml b/app/src/main/res/drawable/resizable_assistant_button.xml deleted file mode 100644 index eeef6ce02..000000000 --- a/app/src/main/res/drawable/resizable_assistant_button.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/round_button_background.xml b/app/src/main/res/drawable/round_button_background.xml deleted file mode 100644 index 5a67aa4b6..000000000 --- a/app/src/main/res/drawable/round_button_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/round_button_background_default.xml b/app/src/main/res/drawable/round_button_background_default.xml deleted file mode 100644 index 63eba8a5c..000000000 --- a/app/src/main/res/drawable/round_button_background_default.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/round_orange_button_background.xml b/app/src/main/res/drawable/round_orange_button_background.xml deleted file mode 100644 index 5c7e546c9..000000000 --- a/app/src/main/res/drawable/round_orange_button_background.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - diff --git a/app/src/main/res/drawable/round_orange_button_background_default.xml b/app/src/main/res/drawable/round_orange_button_background_default.xml deleted file mode 100644 index 211384486..000000000 --- a/app/src/main/res/drawable/round_orange_button_background_default.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/round_orange_button_background_disabled.xml b/app/src/main/res/drawable/round_orange_button_background_disabled.xml deleted file mode 100644 index 0ec6324be..000000000 --- a/app/src/main/res/drawable/round_orange_button_background_disabled.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/round_orange_button_background_over.xml b/app/src/main/res/drawable/round_orange_button_background_over.xml deleted file mode 100644 index abad4e866..000000000 --- a/app/src/main/res/drawable/round_orange_button_background_over.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/round_recording_button_background_dark.xml b/app/src/main/res/drawable/round_recording_button_background_dark.xml deleted file mode 100644 index 70fdb4900..000000000 --- a/app/src/main/res/drawable/round_recording_button_background_dark.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/round_recording_button_background_light.xml b/app/src/main/res/drawable/round_recording_button_background_light.xml deleted file mode 100644 index d6194f718..000000000 --- a/app/src/main/res/drawable/round_recording_button_background_light.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/scroll_to_bottom.xml b/app/src/main/res/drawable/scroll_to_bottom.xml deleted file mode 100644 index 30ca8234c..000000000 --- a/app/src/main/res/drawable/scroll_to_bottom.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/select_all.xml b/app/src/main/res/drawable/select_all.xml deleted file mode 100644 index 294ddea41..000000000 --- a/app/src/main/res/drawable/select_all.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/settings_advanced.xml b/app/src/main/res/drawable/settings_advanced.xml deleted file mode 100644 index 4d26576b3..000000000 --- a/app/src/main/res/drawable/settings_advanced.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/settings_audio.xml b/app/src/main/res/drawable/settings_audio.xml deleted file mode 100644 index bd1f54345..000000000 --- a/app/src/main/res/drawable/settings_audio.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/settings_call.xml b/app/src/main/res/drawable/settings_call.xml deleted file mode 100644 index 1eaeea4cd..000000000 --- a/app/src/main/res/drawable/settings_call.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/settings_chat.xml b/app/src/main/res/drawable/settings_chat.xml deleted file mode 100644 index 91f3146d7..000000000 --- a/app/src/main/res/drawable/settings_chat.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/settings_conferences.xml b/app/src/main/res/drawable/settings_conferences.xml deleted file mode 100644 index 1c4d4d873..000000000 --- a/app/src/main/res/drawable/settings_conferences.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/settings_contacts.xml b/app/src/main/res/drawable/settings_contacts.xml deleted file mode 100644 index 134672d7d..000000000 --- a/app/src/main/res/drawable/settings_contacts.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/settings_network.xml b/app/src/main/res/drawable/settings_network.xml deleted file mode 100644 index 718e0d587..000000000 --- a/app/src/main/res/drawable/settings_network.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/drawable/settings_video.xml b/app/src/main/res/drawable/settings_video.xml deleted file mode 100644 index 9d840fda9..000000000 --- a/app/src/main/res/drawable/settings_video.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/drawable/shape_audio_only_me_background.xml b/app/src/main/res/drawable/shape_audio_only_me_background.xml deleted file mode 100644 index 86c42db08..000000000 --- a/app/src/main/res/drawable/shape_audio_only_me_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_audio_only_remote_background.xml b/app/src/main/res/drawable/shape_audio_only_remote_background.xml deleted file mode 100644 index ec1d610aa..000000000 --- a/app/src/main/res/drawable/shape_audio_only_remote_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_audio_routes_background.xml b/app/src/main/res/drawable/shape_audio_routes_background.xml deleted file mode 100644 index 25038835b..000000000 --- a/app/src/main/res/drawable/shape_audio_routes_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_button_background.xml b/app/src/main/res/drawable/shape_button_background.xml deleted file mode 100644 index b12bba2b6..000000000 --- a/app/src/main/res/drawable/shape_button_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_button_disabled_background.xml b/app/src/main/res/drawable/shape_button_disabled_background.xml deleted file mode 100644 index b9a64291c..000000000 --- a/app/src/main/res/drawable/shape_button_disabled_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_button_pressed_background.xml b/app/src/main/res/drawable/shape_button_pressed_background.xml deleted file mode 100644 index adcc6ce8b..000000000 --- a/app/src/main/res/drawable/shape_button_pressed_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_answer_background.xml b/app/src/main/res/drawable/shape_call_answer_background.xml deleted file mode 100644 index f456f09ef..000000000 --- a/app/src/main/res/drawable/shape_call_answer_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_answer_pressed_background.xml b/app/src/main/res/drawable/shape_call_answer_pressed_background.xml deleted file mode 100644 index 72b56a9a5..000000000 --- a/app/src/main/res/drawable/shape_call_answer_pressed_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_numpad_background.xml b/app/src/main/res/drawable/shape_call_numpad_background.xml deleted file mode 100644 index d5a2c3cd7..000000000 --- a/app/src/main/res/drawable/shape_call_numpad_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_numpad_pressed_background.xml b/app/src/main/res/drawable/shape_call_numpad_pressed_background.xml deleted file mode 100644 index 8e2fd88e2..000000000 --- a/app/src/main/res/drawable/shape_call_numpad_pressed_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_popup_background.xml b/app/src/main/res/drawable/shape_call_popup_background.xml deleted file mode 100644 index 9c9303055..000000000 --- a/app/src/main/res/drawable/shape_call_popup_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_recording_off_background.xml b/app/src/main/res/drawable/shape_call_recording_off_background.xml deleted file mode 100644 index 8fb61094a..000000000 --- a/app/src/main/res/drawable/shape_call_recording_off_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_recording_on_background.xml b/app/src/main/res/drawable/shape_call_recording_on_background.xml deleted file mode 100644 index 35da9f606..000000000 --- a/app/src/main/res/drawable/shape_call_recording_on_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_terminate_background.xml b/app/src/main/res/drawable/shape_call_terminate_background.xml deleted file mode 100644 index a8b903a58..000000000 --- a/app/src/main/res/drawable/shape_call_terminate_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_call_terminate_pressed_background.xml b/app/src/main/res/drawable/shape_call_terminate_pressed_background.xml deleted file mode 100644 index 622eae85f..000000000 --- a/app/src/main/res/drawable/shape_call_terminate_pressed_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_conference_active_speaker_border.xml b/app/src/main/res/drawable/shape_conference_active_speaker_border.xml deleted file mode 100644 index 98a0c1b7f..000000000 --- a/app/src/main/res/drawable/shape_conference_active_speaker_border.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_conference_audio_only_border.xml b/app/src/main/res/drawable/shape_conference_audio_only_border.xml deleted file mode 100644 index d276b1039..000000000 --- a/app/src/main/res/drawable/shape_conference_audio_only_border.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_conference_invite_background.xml b/app/src/main/res/drawable/shape_conference_invite_background.xml deleted file mode 100644 index 041c8baa4..000000000 --- a/app/src/main/res/drawable/shape_conference_invite_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_dialog_background.xml b/app/src/main/res/drawable/shape_dialog_background.xml deleted file mode 100644 index 1321d5ec0..000000000 --- a/app/src/main/res/drawable/shape_dialog_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_dialog_handle.xml b/app/src/main/res/drawable/shape_dialog_handle.xml deleted file mode 100644 index 030e5bcf1..000000000 --- a/app/src/main/res/drawable/shape_dialog_handle.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_edittext_background.xml b/app/src/main/res/drawable/shape_edittext_background.xml deleted file mode 100644 index 825ef744b..000000000 --- a/app/src/main/res/drawable/shape_edittext_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_extra_buttons_background.xml b/app/src/main/res/drawable/shape_extra_buttons_background.xml deleted file mode 100644 index 294ee7cb4..000000000 --- a/app/src/main/res/drawable/shape_extra_buttons_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_form_field_background.xml b/app/src/main/res/drawable/shape_form_field_background.xml deleted file mode 100644 index fce78c8c5..000000000 --- a/app/src/main/res/drawable/shape_form_field_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_me_background.xml b/app/src/main/res/drawable/shape_me_background.xml deleted file mode 100644 index 1efdf15a6..000000000 --- a/app/src/main/res/drawable/shape_me_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_orange_circle.xml b/app/src/main/res/drawable/shape_orange_circle.xml deleted file mode 100644 index d894e038a..000000000 --- a/app/src/main/res/drawable/shape_orange_circle.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_own_reaction_background.xml b/app/src/main/res/drawable/shape_own_reaction_background.xml deleted file mode 100644 index 9821f0651..000000000 --- a/app/src/main/res/drawable/shape_own_reaction_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rect_gray_button.xml b/app/src/main/res/drawable/shape_rect_gray_button.xml deleted file mode 100644 index 7bd9f74ca..000000000 --- a/app/src/main/res/drawable/shape_rect_gray_button.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rect_green_button.xml b/app/src/main/res/drawable/shape_rect_green_button.xml deleted file mode 100644 index 0a51a8db5..000000000 --- a/app/src/main/res/drawable/shape_rect_green_button.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rect_light_gray_button.xml b/app/src/main/res/drawable/shape_rect_light_gray_button.xml deleted file mode 100644 index 6e2eaa7ba..000000000 --- a/app/src/main/res/drawable/shape_rect_light_gray_button.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rect_orange_button.xml b/app/src/main/res/drawable/shape_rect_orange_button.xml deleted file mode 100644 index e239fdbfc..000000000 --- a/app/src/main/res/drawable/shape_rect_orange_button.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_rect_orange_disabled_button.xml b/app/src/main/res/drawable/shape_rect_orange_disabled_button.xml deleted file mode 100644 index 7928e4f48..000000000 --- a/app/src/main/res/drawable/shape_rect_orange_disabled_button.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_remote_background.xml b/app/src/main/res/drawable/shape_remote_background.xml deleted file mode 100644 index f77318418..000000000 --- a/app/src/main/res/drawable/shape_remote_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_remote_paused_background.xml b/app/src/main/res/drawable/shape_remote_paused_background.xml deleted file mode 100644 index 8663b555c..000000000 --- a/app/src/main/res/drawable/shape_remote_paused_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_remote_recording_background.xml b/app/src/main/res/drawable/shape_remote_recording_background.xml deleted file mode 100644 index 7247f620a..000000000 --- a/app/src/main/res/drawable/shape_remote_recording_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_remote_video_background.xml b/app/src/main/res/drawable/shape_remote_video_background.xml deleted file mode 100644 index 6b924bcef..000000000 --- a/app/src/main/res/drawable/shape_remote_video_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_button.xml b/app/src/main/res/drawable/shape_round_button.xml deleted file mode 100644 index e4607cb25..000000000 --- a/app/src/main/res/drawable/shape_round_button.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_button_disabled.xml b/app/src/main/res/drawable/shape_round_button_disabled.xml deleted file mode 100644 index b9a64291c..000000000 --- a/app/src/main/res/drawable/shape_round_button_disabled.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_button_pressed.xml b/app/src/main/res/drawable/shape_round_button_pressed.xml deleted file mode 100644 index adcc6ce8b..000000000 --- a/app/src/main/res/drawable/shape_round_button_pressed.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_dark_gray_background.xml b/app/src/main/res/drawable/shape_round_dark_gray_background.xml deleted file mode 100644 index d1cf0fab4..000000000 --- a/app/src/main/res/drawable/shape_round_dark_gray_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_dark_gray_background_with_orange_border.xml b/app/src/main/res/drawable/shape_round_dark_gray_background_with_orange_border.xml deleted file mode 100644 index dfd68923b..000000000 --- a/app/src/main/res/drawable/shape_round_dark_gray_background_with_orange_border.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_gray_background.xml b/app/src/main/res/drawable/shape_round_gray_background.xml deleted file mode 100644 index adc21b355..000000000 --- a/app/src/main/res/drawable/shape_round_gray_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_gray_background_with_orange_border.xml b/app/src/main/res/drawable/shape_round_gray_background_with_orange_border.xml deleted file mode 100644 index 2dd378d80..000000000 --- a/app/src/main/res/drawable/shape_round_gray_background_with_orange_border.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_red_background.xml b/app/src/main/res/drawable/shape_round_red_background.xml deleted file mode 100644 index 9cc11d1e7..000000000 --- a/app/src/main/res/drawable/shape_round_red_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_round_red_background_with_orange_border.xml b/app/src/main/res/drawable/shape_round_red_background_with_orange_border.xml deleted file mode 100644 index 033bc92ea..000000000 --- a/app/src/main/res/drawable/shape_round_red_background_with_orange_border.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_toggle_disabled_background.xml b/app/src/main/res/drawable/shape_toggle_disabled_background.xml deleted file mode 100644 index b9a64291c..000000000 --- a/app/src/main/res/drawable/shape_toggle_disabled_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_toggle_off_background.xml b/app/src/main/res/drawable/shape_toggle_off_background.xml deleted file mode 100644 index b12bba2b6..000000000 --- a/app/src/main/res/drawable/shape_toggle_off_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_toggle_on_background.xml b/app/src/main/res/drawable/shape_toggle_on_background.xml deleted file mode 100644 index e4607cb25..000000000 --- a/app/src/main/res/drawable/shape_toggle_on_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_toggle_pressed_background.xml b/app/src/main/res/drawable/shape_toggle_pressed_background.xml deleted file mode 100644 index adcc6ce8b..000000000 --- a/app/src/main/res/drawable/shape_toggle_pressed_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_transparent_reaction_background.xml b/app/src/main/res/drawable/shape_transparent_reaction_background.xml deleted file mode 100644 index 15e76c3b7..000000000 --- a/app/src/main/res/drawable/shape_transparent_reaction_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/status_led_background.xml b/app/src/main/res/drawable/status_led_background.xml deleted file mode 100644 index 7147daf3c..000000000 --- a/app/src/main/res/drawable/status_led_background.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/unread_message_count_bg.xml b/app/src/main/res/drawable/unread_message_count_bg.xml deleted file mode 100644 index 940a12693..000000000 --- a/app/src/main/res/drawable/unread_message_count_bg.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/drawable/valid.xml b/app/src/main/res/drawable/valid.xml deleted file mode 100644 index 3be5be72e..000000000 --- a/app/src/main/res/drawable/valid.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/vector_linphone_logo.xml b/app/src/main/res/drawable/vector_linphone_logo.xml deleted file mode 100644 index 9c1d994f9..000000000 --- a/app/src/main/res/drawable/vector_linphone_logo.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout-land/about_fragment.xml b/app/src/main/res/layout-land/about_fragment.xml deleted file mode 100644 index 3a351bf9f..000000000 --- a/app/src/main/res/layout-land/about_fragment.xml +++ /dev/null @@ -1,191 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/assistant_welcome_fragment.xml b/app/src/main/res/layout-land/assistant_welcome_fragment.xml deleted file mode 100644 index d89ae4936..000000000 --- a/app/src/main/res/layout-land/assistant_welcome_fragment.xml +++ /dev/null @@ -1,166 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-land/conference_waiting_room_fragment.xml b/app/src/main/res/layout-land/conference_waiting_room_fragment.xml deleted file mode 100644 index 64c9d3c78..000000000 --- a/app/src/main/res/layout-land/conference_waiting_room_fragment.xml +++ /dev/null @@ -1,312 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/dialer_fragment.xml b/app/src/main/res/layout-land/dialer_fragment.xml deleted file mode 100644 index a911071a9..000000000 --- a/app/src/main/res/layout-land/dialer_fragment.xml +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-land/main_activity_content.xml b/app/src/main/res/layout-land/main_activity_content.xml deleted file mode 100644 index 016fb5554..000000000 --- a/app/src/main/res/layout-land/main_activity_content.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/tabs_fragment.xml b/app/src/main/res/layout-land/tabs_fragment.xml deleted file mode 100644 index 72e54530a..000000000 --- a/app/src/main/res/layout-land/tabs_fragment.xml +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/voip_conference_active_speaker.xml b/app/src/main/res/layout-land/voip_conference_active_speaker.xml deleted file mode 100644 index df24c7f74..000000000 --- a/app/src/main/res/layout-land/voip_conference_active_speaker.xml +++ /dev/null @@ -1,363 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/voip_conference_grid.xml b/app/src/main/res/layout-land/voip_conference_grid.xml deleted file mode 100644 index ecac814f9..000000000 --- a/app/src/main/res/layout-land/voip_conference_grid.xml +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-land/voip_numpad.xml b/app/src/main/res/layout-land/voip_numpad.xml deleted file mode 100644 index 3e21ebf53..000000000 --- a/app/src/main/res/layout-land/voip_numpad.xml +++ /dev/null @@ -1,225 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout-sw533dp-land/dialer_fragment.xml b/app/src/main/res/layout-sw533dp-land/dialer_fragment.xml deleted file mode 100644 index 50137dd74..000000000 --- a/app/src/main/res/layout-sw533dp-land/dialer_fragment.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout-sw600dp/dialer_fragment.xml b/app/src/main/res/layout-sw600dp/dialer_fragment.xml deleted file mode 100644 index 4c513ca49..000000000 --- a/app/src/main/res/layout-sw600dp/dialer_fragment.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/about_fragment.xml b/app/src/main/res/layout/about_fragment.xml deleted file mode 100644 index 6198b5301..000000000 --- a/app/src/main/res/layout/about_fragment.xml +++ /dev/null @@ -1,197 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/assistant_account_login_fragment.xml b/app/src/main/res/layout/assistant_account_login_fragment.xml deleted file mode 100644 index b6051be74..000000000 --- a/app/src/main/res/layout/assistant_account_login_fragment.xml +++ /dev/null @@ -1,282 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/assistant_activity.xml b/app/src/main/res/layout/assistant_activity.xml deleted file mode 100644 index 3b2192ec4..000000000 --- a/app/src/main/res/layout/assistant_activity.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/assistant_country_picker_cell.xml b/app/src/main/res/layout/assistant_country_picker_cell.xml deleted file mode 100644 index ccc3d74d9..000000000 --- a/app/src/main/res/layout/assistant_country_picker_cell.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/assistant_country_picker_fragment.xml b/app/src/main/res/layout/assistant_country_picker_fragment.xml deleted file mode 100644 index a387259a4..000000000 --- a/app/src/main/res/layout/assistant_country_picker_fragment.xml +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/assistant_echo_canceller_calibration_fragment.xml b/app/src/main/res/layout/assistant_echo_canceller_calibration_fragment.xml deleted file mode 100644 index acf86c157..000000000 --- a/app/src/main/res/layout/assistant_echo_canceller_calibration_fragment.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/assistant_email_account_creation_fragment.xml b/app/src/main/res/layout/assistant_email_account_creation_fragment.xml deleted file mode 100644 index e690c83d5..000000000 --- a/app/src/main/res/layout/assistant_email_account_creation_fragment.xml +++ /dev/null @@ -1,169 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/assistant_email_account_validation_fragment.xml b/app/src/main/res/layout/assistant_email_account_validation_fragment.xml deleted file mode 100644 index 6a487897e..000000000 --- a/app/src/main/res/layout/assistant_email_account_validation_fragment.xml +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/assistant_generic_account_login_fragment.xml b/app/src/main/res/layout/assistant_generic_account_login_fragment.xml deleted file mode 100644 index 86376a5eb..000000000 --- a/app/src/main/res/layout/assistant_generic_account_login_fragment.xml +++ /dev/null @@ -1,189 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/assistant_generic_account_warning_fragment.xml b/app/src/main/res/layout/assistant_generic_account_warning_fragment.xml deleted file mode 100644 index b4ab8d0c1..000000000 --- a/app/src/main/res/layout/assistant_generic_account_warning_fragment.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - -