diff --git a/app/build.gradle b/app/build.gradle
index f3f61580b..bbb8869ca 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,5 +1,13 @@
apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+
+apply plugin: 'kotlin-android-extensions'
+
+apply plugin: 'kotlin-kapt'
+
+apply plugin: "org.jlleitschuh.gradle.ktlint"
+
static def getPackageName() {
return "org.linphone"
}
@@ -10,7 +18,7 @@ static def firebaseEnabled() {
}
task getGitVersion() {
- def gitVersion = "4.4.0"
+ def gitVersion = "5.0"
def gitVersionStream = new ByteArrayOutputStream()
def gitCommitsCount = new ByteArrayOutputStream()
def gitCommitHash = new ByteArrayOutputStream()
@@ -41,69 +49,18 @@ task getGitVersion() {
project.version = gitVersion
}
-configurations {
- customImpl.extendsFrom(implementation)
-}
-
-task linphoneSdkSource() {
- doLast {
- configurations.customImpl.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"]
- }
- }
- }
- }
-}
-
-///// Exclude Files /////
-
-def excludeFiles = []
-if (!firebaseEnabled()) {
- excludeFiles.add('**/Firebase*')
- println '[Push Notification] Firebase disabled'
-}
-// Remove or comment if you want to use those
-excludeFiles.add('**/XmlRpc*')
-excludeFiles.add('**/InAppPurchase*')
-
-def excludePackage = []
-
-excludePackage.add('**/gdb.*')
-excludePackage.add('**/libopenh264**')
-excludePackage.add('**/**tester**')
-excludePackage.add('**/LICENSE.txt')
-
-/////////////////////////
-
-repositories {
- maven {
- name "local linphone-sdk maven repository"
- url file(LinphoneSdkBuildDir + '/maven_repository/')
- }
- maven {
- name "linphone.org maven repository"
- url "https://linphone.org/maven_repository"
- }
-}
-
project.tasks['preBuild'].dependsOn 'getGitVersion'
project.tasks['preBuild'].dependsOn 'linphoneSdkSource'
android {
- lintOptions {
- abortOnError false
- }
-
compileSdkVersion 29
+ buildToolsVersion "29.0.2"
defaultConfig {
minSdkVersion 23
targetSdkVersion 29
- versionCode 4400
+ versionCode 4300
versionName "${project.version}"
applicationId getPackageName()
- multiDexEnabled true
}
applicationVariants.all { variant ->
@@ -114,12 +71,10 @@ android {
// https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for extractNativeLibs
if (variant.buildType.name == "release") {
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
- linphone_file_provider: getPackageName() + ".provider",
- extractNativeLibs: "false"]
+ linphone_file_provider: getPackageName() + ".fileprovider"]
} else {
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
- linphone_file_provider: getPackageName() + ".debug.provider",
- extractNativeLibs: "true"]
+ linphone_file_provider: getPackageName() + ".debug.fileprovider"]
}
}
@@ -143,20 +98,21 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
resValue "string", "sync_account_type", getPackageName() + ".sync"
- resValue "string", "file_provider", getPackageName() + ".provider"
+ resValue "string", "file_provider", getPackageName() + ".fileprovider"
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
if (!firebaseEnabled()) {
resValue "string", "gcm_defaultSenderId", "none"
}
}
+
debug {
applicationIdSuffix ".debug"
debuggable true
jniDebuggable true
resValue "string", "sync_account_type", getPackageName() + ".sync"
- resValue "string", "file_provider", getPackageName() + ".debug.provider"
+ resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
if (!firebaseEnabled()) {
@@ -165,35 +121,44 @@ android {
}
}
- sourceSets {
- main {
- java.excludes = excludeFiles
+ dataBinding {
+ enabled = true
+ }
+}
- packagingOptions {
- excludes = excludePackage
- }
- }
+repositories {
+ maven {
+ url file(LinphoneSdkBuildDir + '/maven_repository/')
}
- packagingOptions {
- pickFirst 'META-INF/NOTICE'
- pickFirst 'META-INF/LICENSE'
- exclude 'META-INF/MANIFEST.MF'
- }
+ /*maven {
+ url "https://linphone.org/maven_repository"
+ }*/
}
dependencies {
- compileOnly 'org.jetbrains:annotations:19.0.0'
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation "androidx.media:media:1.1.0"
+ implementation 'androidx.fragment:fragment-ktx:1.2.3'
+ implementation 'androidx.core:core-ktx:1.2.0'
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.2.1'
+ implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
+ implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
+ implementation 'androidx.recyclerview:recyclerview:1.1.0'
+ implementation 'com.google.android:flexbox:2.0.0'
+ implementation 'com.github.bumptech.glide:glide:4.11.0'
+
if (firebaseEnabled()) {
implementation 'com.google.firebase:firebase-messaging:19.0.1'
}
- implementation 'androidx.media:media:1.2.0'
- implementation 'androidx.recyclerview:recyclerview:1.0.0'
- implementation 'androidx.appcompat:appcompat:1.1.0'
- implementation 'com.google.android:flexbox:1.1.0'
- implementation 'com.github.bumptech.glide:glide:4.9.0'
- implementation "org.linphone:linphone-sdk-android:4.5+"
+
+ implementation 'org.linphone:linphone-sdk-android:4.4+'
}
+
if (firebaseEnabled()) {
apply plugin: 'com.google.gms.google-services'
}
@@ -210,12 +175,9 @@ task generateContactsXml(type: Copy) {
}
project.tasks['preBuild'].dependsOn 'generateContactsXml'
-apply plugin: "com.diffplug.gradle.spotless"
-spotless {
- java {
- target '**/*.java'
- googleJavaFormat('1.6').aosp()
- removeUnusedImports()
- }
+ktlint {
+ android = true
+ ignoreFailures = true
}
-project.tasks['preBuild'].dependsOn 'spotlessApply'
+
+project.tasks['preBuild'].dependsOn 'ktlintFormat'
\ No newline at end of file
diff --git a/app/contacts.xml b/app/contacts.xml
index 766be1f0b..07ea82c10 100644
--- a/app/contacts.xml
+++ b/app/contacts.xml
@@ -4,7 +4,7 @@
diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro
index efab2ccf6..f1b424510 100644
--- a/app/proguard-rules.pro
+++ b/app/proguard-rules.pro
@@ -1 +1,21 @@
--dontwarn org.apache.**
+# 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
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
old mode 100755
new mode 100644
index 05f552aef..b726b0b48
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,222 +1,122 @@
+ package="org.linphone">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+ android:theme="@style/AppTheme">
+ android:name=".activities.launcher.LauncherActivity"
+ android:noHistory="true"
+ android:theme="@style/LauncherTheme">
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
+
+
+
+
+
-
+
-
+
+
+ android:turnScreenOn="true" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:noHistory="true" />
+ android:name=".core.CoreService"
+ android:foregroundServiceType="phoneCall"
+ android:stopWithTask="false"
+ android:label="@string/app_name" />
+
+
+
+
+
+
@@ -224,13 +124,13 @@
+ android:resource="@xml/sync_adapter" />
-
+
@@ -240,21 +140,11 @@
android:resource="@xml/authenticator" />
-
-
-
-
-
-
-
+
-
-
-
+
@@ -263,10 +153,10 @@
android:enabled="true"
android:exported="false" />
-
+
-
+
+
@@ -283,4 +173,5 @@
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/raw/default_assistant_create.rc b/app/src/main/assets/assistant_default_values
similarity index 91%
rename from app/src/main/res/raw/default_assistant_create.rc
rename to app/src/main/assets/assistant_default_values
index dc66cd4da..7e5d7543b 100644
--- a/app/src/main/res/raw/default_assistant_create.rc
+++ b/app/src/main/assets/assistant_default_values
@@ -21,12 +21,6 @@
-
-
MD5
diff --git a/app/src/main/res/raw/linphone_assistant_create.rc b/app/src/main/assets/assistant_linphone_default_values
similarity index 100%
rename from app/src/main/res/raw/linphone_assistant_create.rc
rename to app/src/main/assets/assistant_linphone_default_values
diff --git a/app/src/main/res/raw/linphonerc_default b/app/src/main/assets/linphonerc_default
similarity index 88%
rename from app/src/main/res/raw/linphonerc_default
rename to app/src/main/assets/linphonerc_default
index 808feee28..1691780b7 100644
--- a/app/src/main/res/raw/linphonerc_default
+++ b/app/src/main/assets/linphonerc_default
@@ -1,3 +1,6 @@
+
+## Start of default rc
+
[sip]
contact="Linphone Android"
use_info=0
@@ -34,4 +37,6 @@ history_max_size=100
[in-app-purchase]
server_url=https://subscribe.linphone.org:444/inapp.php
-purchasable_items_ids=test_account_subscription
\ No newline at end of file
+purchasable_items_ids=test_account_subscription
+
+## End of default rc
diff --git a/app/src/main/res/raw/linphonerc_factory b/app/src/main/assets/linphonerc_factory
similarity index 92%
rename from app/src/main/res/raw/linphonerc_factory
rename to app/src/main/assets/linphonerc_factory
index a9a51cb5a..3855f2db0 100644
--- a/app/src/main/res/raw/linphonerc_factory
+++ b/app/src/main/assets/linphonerc_factory
@@ -1,7 +1,9 @@
-#
+## 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
@@ -36,4 +38,6 @@ prefer_basic_chat_room=1
xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
[lime]
-lime_update_threshold=-1
\ No newline at end of file
+lime_update_threshold=-1
+
+## End of factory rc
diff --git a/app/src/main/java/com/android/vending/billing/IInAppBillingService.aidl b/app/src/main/java/com/android/vending/billing/IInAppBillingService.aidl
deleted file mode 100644
index 2a492f784..000000000
--- a/app/src/main/java/com/android/vending/billing/IInAppBillingService.aidl
+++ /dev/null
@@ -1,144 +0,0 @@
-/*
- * Copyright (C) 2012 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.vending.billing;
-
-import android.os.Bundle;
-
-/**
- * InAppBillingService is the service that provides in-app billing version 3 and beyond.
- * This service provides the following features:
- * 1. Provides a new API to get details of in-app items published for the app including
- * price, type, title and description.
- * 2. The purchase flow is synchronous and purchase information is available immediately
- * after it completes.
- * 3. Purchase information of in-app purchases is maintained within the Google Play system
- * till the purchase is consumed.
- * 4. An API to consume a purchase of an inapp item. All purchases of one-time
- * in-app items are consumable and thereafter can be purchased again.
- * 5. An API to get current purchases of the user immediately. This will not contain any
- * consumed purchases.
- *
- * All calls will give a response code with the following possible values
- * RESULT_OK = 0 - success
- * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
- * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
- * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
- * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
- * RESULT_ERROR = 6 - Fatal error during the API action
- * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
- * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
- */
-interface IInAppBillingService {
- /**
- * Checks support for the requested billing API version, package and in-app type.
- * Minimum API version supported by this interface is 3.
- * @param apiVersion the billing version which the app is using
- * @param packageName the package name of the calling app
- * @param type type of the in-app item being purchased "inapp" for one-time purchases
- * and "subs" for subscription.
- * @return RESULT_OK(0) on success, corresponding result code on failures
- */
- int isBillingSupported(int apiVersion, String packageName, String type);
-
- /**
- * Provides details of a list of SKUs
- * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
- * with a list JSON strings containing the productId, price, title and description.
- * This API can be called with a maximum of 20 SKUs.
- * @param apiVersion billing API version that the Third-party is using
- * @param packageName the package name of the calling app
- * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
- * @return Bundle containing the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "DETAILS_LIST" with a StringArrayList containing purchase information
- * in JSON format similar to:
- * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
- * "title : "Example Title", "description" : "This is an example description" }'
- */
- Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
-
- /**
- * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
- * the type, a unique purchase token and an optional developer payload.
- * @param apiVersion billing API version that the app is using
- * @param packageName package name of the calling app
- * @param sku the SKU of the in-app item as published in the developer console
- * @param type the type of the in-app item ("inapp" for one-time purchases
- * and "subs" for subscription).
- * @param developerPayload optional argument to be sent back with the purchase information
- * @return Bundle containing the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "BUY_INTENT" - PendingIntent to start the purchase flow
- *
- * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
- * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
- * If the purchase is successful, the result data will contain the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "INAPP_PURCHASE_DATA" - String in JSON format similar to
- * '{"orderId":"12999763169054705758.1371079406387615",
- * "packageName":"com.example.app",
- * "productId":"exampleSku",
- * "purchaseTime":1345678900000,
- * "purchaseToken" : "122333444455555",
- * "developerPayload":"example developer payload" }'
- * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
- * was signed with the private key of the developer
- * TODO: change this to app-specific keys.
- */
- Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
- String developerPayload);
-
- /**
- * Returns the current SKUs owned by the user of the type and package name specified along with
- * purchase information and a signature of the data to be validated.
- * This will return all SKUs that have been purchased in V3 and managed items purchased using
- * V1 and V2 that have not been consumed.
- * @param apiVersion billing API version that the app is using
- * @param packageName package name of the calling app
- * @param type the type of the in-app items being requested
- * ("inapp" for one-time purchases and "subs" for subscription).
- * @param continuationToken to be set as null for the first call, if the number of owned
- * skus are too many, a continuationToken is returned in the response bundle.
- * This method can be called again with the continuation token to get the next set of
- * owned skus.
- * @return Bundle containing the following key-value pairs
- * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
- * failure as listed above.
- * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
- * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
- * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
- * of the purchase information
- * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
- * next set of in-app purchases. Only set if the
- * user has more owned skus than the current list.
- */
- Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
-
- /**
- * Consume the last purchase of the given SKU. This will result in this item being removed
- * from all subsequent responses to getPurchases() and allow re-purchase of this item.
- * @param apiVersion billing API version that the app is using
- * @param packageName package name of the calling app
- * @param purchaseToken token in the purchase information JSON that identifies the purchase
- * to be consumed
- * @return 0 if consumption succeeded. Appropriate error values for failures.
- */
- int consumePurchase(int apiVersion, String packageName, String purchaseToken);
-}
diff --git a/app/src/main/java/org/linphone/LinphoneApplication.kt b/app/src/main/java/org/linphone/LinphoneApplication.kt
new file mode 100644
index 000000000..907968f90
--- /dev/null
+++ b/app/src/main/java/org/linphone/LinphoneApplication.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.app.Application
+import org.linphone.core.CoreContext
+import org.linphone.core.CorePreferences
+import org.linphone.core.Factory
+import org.linphone.core.LogCollectionState
+import org.linphone.core.tools.Log
+
+class LinphoneApplication : Application() {
+ companion object {
+ lateinit var corePreferences: CorePreferences
+ lateinit var coreContext: CoreContext
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ val appName = getString(R.string.app_name)
+ android.util.Log.i("[$appName]", "Application is being created")
+
+ Factory.instance().setLogCollectionPath(applicationContext.filesDir.absolutePath)
+ Factory.instance().enableLogCollection(LogCollectionState.Enabled)
+
+ corePreferences = CorePreferences(applicationContext)
+ corePreferences.copyAssetsFromPackage()
+
+ val config = Factory.instance().createConfigWithFactory(corePreferences.configPath, corePreferences.factoryConfigPath)
+ corePreferences.config = config
+
+ Factory.instance().setDebugMode(corePreferences.debugLogs, appName)
+
+ coreContext = CoreContext(applicationContext, config)
+ coreContext.start()
+ Log.i("[Application] Created")
+ }
+}
diff --git a/app/src/main/java/org/linphone/LinphoneContext.java b/app/src/main/java/org/linphone/LinphoneContext.java
deleted file mode 100644
index 61ea0ed2e..000000000
--- a/app/src/main/java/org/linphone/LinphoneContext.java
+++ /dev/null
@@ -1,342 +0,0 @@
-/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for 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 static android.content.Intent.ACTION_MAIN;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Build;
-import android.provider.ContactsContract;
-import java.util.ArrayList;
-import org.linphone.call.CallActivity;
-import org.linphone.call.CallIncomingActivity;
-import org.linphone.call.CallOutgoingActivity;
-import org.linphone.compatibility.Compatibility;
-import org.linphone.contacts.ContactsManager;
-import org.linphone.core.Call;
-import org.linphone.core.ConfiguringState;
-import org.linphone.core.Core;
-import org.linphone.core.CoreListenerStub;
-import org.linphone.core.Factory;
-import org.linphone.core.GlobalState;
-import org.linphone.core.LogLevel;
-import org.linphone.core.LoggingService;
-import org.linphone.core.LoggingServiceListener;
-import org.linphone.core.tools.Log;
-import org.linphone.mediastream.Version;
-import org.linphone.notifications.NotificationsManager;
-import org.linphone.service.LinphoneService;
-import org.linphone.settings.LinphonePreferences;
-import org.linphone.utils.DeviceUtils;
-import org.linphone.utils.LinphoneUtils;
-import org.linphone.utils.PushNotificationUtils;
-
-public class LinphoneContext {
- private static LinphoneContext sInstance = null;
-
- private Context mContext;
-
- private final LoggingServiceListener mJavaLoggingService =
- new LoggingServiceListener() {
- @Override
- public void onLogMessageWritten(
- LoggingService logService, String domain, LogLevel lev, String message) {
- switch (lev) {
- case Debug:
- android.util.Log.d(domain, message);
- break;
- case Message:
- android.util.Log.i(domain, message);
- break;
- case Warning:
- android.util.Log.w(domain, message);
- break;
- case Error:
- android.util.Log.e(domain, message);
- break;
- case Fatal:
- default:
- android.util.Log.wtf(domain, message);
- break;
- }
- }
- };
- private CoreListenerStub mListener;
- private NotificationsManager mNotificationManager;
- private LinphoneManager mLinphoneManager;
- private ContactsManager mContactsManager;
- private final ArrayList mCoreStartedListeners;
-
- public static boolean isReady() {
- return sInstance != null;
- }
-
- public static LinphoneContext instance() {
- if (sInstance == null) {
- throw new RuntimeException("[Context] Linphone Context not available!");
- }
- return sInstance;
- }
-
- public LinphoneContext(Context context) {
- mContext = context;
- mCoreStartedListeners = new ArrayList<>();
-
- LinphonePreferences.instance().setContext(context);
- Factory.instance().setLogCollectionPath(context.getFilesDir().getAbsolutePath());
- boolean isDebugEnabled = LinphonePreferences.instance().isDebugEnabled();
- LinphoneUtils.configureLoggingService(isDebugEnabled, context.getString(R.string.app_name));
-
- // Dump some debugging information to the logs
- dumpDeviceInformation();
- dumpLinphoneInformation();
-
- sInstance = this;
- Log.i("[Context] Ready");
-
- mListener =
- new CoreListenerStub() {
- @Override
- public void onGlobalStateChanged(Core core, GlobalState state, String message) {
- Log.i("[Context] Global state is [", state, "]");
-
- if (state == GlobalState.On) {
- for (CoreStartedListener listener : mCoreStartedListeners) {
- listener.onCoreStarted();
- }
- }
- }
-
- @Override
- public void onConfiguringStatus(
- Core core, ConfiguringState status, String message) {
- Log.i("[Context] Configuring state is [", status, "]");
-
- if (status == ConfiguringState.Successful) {
- LinphonePreferences.instance()
- .setPushNotificationEnabled(
- LinphonePreferences.instance()
- .isPushNotificationEnabled());
- }
- }
-
- @Override
- public void onCallStateChanged(
- Core core, Call call, Call.State state, String message) {
- Log.i("[Context] Call state is [", state, "]");
-
- if (mContext.getResources().getBoolean(R.bool.enable_call_notification)) {
- mNotificationManager.displayCallNotification(call);
- }
-
- if (state == Call.State.IncomingReceived
- || state == Call.State.IncomingEarlyMedia) {
- // Starting SDK 24 (Android 7.0) we rely on the fullscreen intent of the
- // call incoming notification
- if (Version.sdkStrictlyBelow(Version.API24_NOUGAT_70)) {
- if (!mLinphoneManager.getCallGsmON()) onIncomingReceived();
- }
-
- // In case of push notification Service won't be started until here
- if (!LinphoneService.isReady()) {
- Log.i("[Context] Service not running, starting it");
- Intent intent = new Intent(ACTION_MAIN);
- intent.setClass(mContext, LinphoneService.class);
- mContext.startService(intent);
- }
- } else if (state == Call.State.OutgoingInit) {
- onOutgoingStarted();
- } else if (state == Call.State.Connected) {
- onCallStarted();
- } else if (state == Call.State.End
- || state == Call.State.Released
- || state == Call.State.Error) {
- if (LinphoneService.isReady()) {
- LinphoneService.instance().destroyOverlay();
- }
-
- if (state == Call.State.Released
- && call.getCallLog().getStatus() == Call.Status.Missed) {
- mNotificationManager.displayMissedCallNotification(call);
- }
- }
- }
- };
-
- mLinphoneManager = new LinphoneManager(context);
- mNotificationManager = new NotificationsManager(context);
-
- if (DeviceUtils.isAppUserRestricted(mContext)) {
- // See https://firebase.google.com/docs/cloud-messaging/android/receive#restricted
- Log.w(
- "[Context] Device has been restricted by user (Android 9+), push notifications won't work !");
- }
-
- int bucket = DeviceUtils.getAppStandbyBucket(mContext);
- if (bucket > 0) {
- Log.w(
- "[Context] Device is in bucket "
- + Compatibility.getAppStandbyBucketNameFromValue(bucket));
- }
-
- if (!PushNotificationUtils.isAvailable(mContext)) {
- Log.w("[Context] Push notifications won't work !");
- }
- }
-
- public void start(boolean isPush) {
- Log.i("[Context] Starting, push status is ", isPush);
- mLinphoneManager.startLibLinphone(isPush, mListener);
-
- mNotificationManager.onCoreReady();
-
- mContactsManager = new ContactsManager(mContext);
- if (!Version.sdkAboveOrEqual(Version.API26_O_80)
- || (mContactsManager.hasReadContactsAccess())) {
- mContext.getContentResolver()
- .registerContentObserver(
- ContactsContract.Contacts.CONTENT_URI, true, mContactsManager);
- }
- if (mContactsManager.hasReadContactsAccess()) {
- mContactsManager.enableContactsAccess();
- }
- mContactsManager.initializeContactManager();
- }
-
- public void destroy() {
- Log.i("[Context] Destroying");
- Core core = LinphoneManager.getCore();
- if (core != null) {
- core.removeListener(mListener);
- core = null; // To allow the gc calls below to free the Core
- }
-
- // Make sure our notification is gone.
- if (mNotificationManager != null) {
- mNotificationManager.destroy();
- }
-
- if (mContactsManager != null) {
- mContactsManager.destroy();
- }
-
- // Destroy the LinphoneManager second to last to ensure any getCore() call will work
- if (mLinphoneManager != null) {
- mLinphoneManager.destroy();
- }
-
- // Wait for every other object to be destroyed to make LinphoneService.instance() invalid
- sInstance = null;
-
- if (LinphonePreferences.instance().useJavaLogger()) {
- Factory.instance().getLoggingService().removeListener(mJavaLoggingService);
- }
- LinphonePreferences.instance().destroy();
- }
-
- public void updateContext(Context context) {
- mContext = context;
- }
-
- public Context getApplicationContext() {
- return mContext;
- }
-
- /* Managers accessors */
-
- public LoggingServiceListener getJavaLoggingService() {
- return mJavaLoggingService;
- }
-
- public NotificationsManager getNotificationManager() {
- return mNotificationManager;
- }
-
- public LinphoneManager getLinphoneManager() {
- return mLinphoneManager;
- }
-
- public ContactsManager getContactsManager() {
- return mContactsManager;
- }
-
- public void addCoreStartedListener(CoreStartedListener listener) {
- mCoreStartedListeners.add(listener);
- }
-
- public void removeCoreStartedListener(CoreStartedListener listener) {
- mCoreStartedListeners.remove(listener);
- }
-
- /* Log device related information */
-
- private void dumpDeviceInformation() {
- Log.i("==== Phone information dump ====");
- Log.i("DISPLAY NAME=" + Compatibility.getDeviceName(mContext));
- Log.i("DEVICE=" + Build.DEVICE);
- Log.i("MODEL=" + Build.MODEL);
- Log.i("MANUFACTURER=" + Build.MANUFACTURER);
- Log.i("ANDROID SDK=" + Build.VERSION.SDK_INT);
- StringBuilder sb = new StringBuilder();
- sb.append("ABIs=");
- for (String abi : Version.getCpuAbis()) {
- sb.append(abi).append(", ");
- }
- Log.i(sb.substring(0, sb.length() - 2));
- }
-
- private void dumpLinphoneInformation() {
- Log.i("==== Linphone information dump ====");
- Log.i("VERSION NAME=" + org.linphone.BuildConfig.VERSION_NAME);
- Log.i("VERSION CODE=" + org.linphone.BuildConfig.VERSION_CODE);
- Log.i("PACKAGE=" + org.linphone.BuildConfig.APPLICATION_ID);
- Log.i("BUILD TYPE=" + org.linphone.BuildConfig.BUILD_TYPE);
- Log.i("SDK VERSION=" + mContext.getString(R.string.linphone_sdk_version));
- Log.i("SDK BRANCH=" + mContext.getString(R.string.linphone_sdk_branch));
- }
-
- /* Call activities */
-
- private void onIncomingReceived() {
- Intent intent = new Intent(mContext, CallIncomingActivity.class);
- // This flag is required to start an Activity from a Service context
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mContext.startActivity(intent);
- }
-
- private void onOutgoingStarted() {
- Intent intent = new Intent(mContext, CallOutgoingActivity.class);
- // This flag is required to start an Activity from a Service context
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mContext.startActivity(intent);
- }
-
- private void onCallStarted() {
- Intent intent = new Intent(mContext, CallActivity.class);
- // This flag is required to start an Activity from a Service context
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mContext.startActivity(intent);
- }
-
- public interface CoreStartedListener {
- void onCoreStarted();
- }
-}
diff --git a/app/src/main/java/org/linphone/LinphoneManager.java b/app/src/main/java/org/linphone/LinphoneManager.java
deleted file mode 100644
index 06bf7a197..000000000
--- a/app/src/main/java/org/linphone/LinphoneManager.java
+++ /dev/null
@@ -1,635 +0,0 @@
-/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for 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.content.Context;
-import android.hardware.Sensor;
-import android.hardware.SensorEvent;
-import android.hardware.SensorEventListener;
-import android.hardware.SensorManager;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.PowerManager;
-import android.os.PowerManager.WakeLock;
-import android.telephony.PhoneStateListener;
-import android.telephony.TelephonyManager;
-import java.io.File;
-import java.util.Timer;
-import java.util.TimerTask;
-import org.linphone.call.AndroidAudioManager;
-import org.linphone.call.CallManager;
-import org.linphone.contacts.ContactsManager;
-import org.linphone.core.AccountCreator;
-import org.linphone.core.Call;
-import org.linphone.core.Call.State;
-import org.linphone.core.Core;
-import org.linphone.core.CoreListener;
-import org.linphone.core.CoreListenerStub;
-import org.linphone.core.Factory;
-import org.linphone.core.FriendList;
-import org.linphone.core.PresenceActivity;
-import org.linphone.core.PresenceBasicStatus;
-import org.linphone.core.PresenceModel;
-import org.linphone.core.ProxyConfig;
-import org.linphone.core.Reason;
-import org.linphone.core.Tunnel;
-import org.linphone.core.TunnelConfig;
-import org.linphone.core.tools.Log;
-import org.linphone.settings.LinphonePreferences;
-import org.linphone.utils.LinphoneUtils;
-import org.linphone.utils.MediaScanner;
-import org.linphone.utils.PushNotificationUtils;
-
-/** Handles Linphone's Core lifecycle */
-public class LinphoneManager implements SensorEventListener {
- private final String mBasePath;
- private final String mRingSoundFile;
- private final String mCallLogDatabaseFile;
- private final String mFriendsDatabaseFile;
- private final String mUserCertsPath;
-
- private final Context mContext;
- private AndroidAudioManager mAudioManager;
- private CallManager mCallManager;
- private final PowerManager mPowerManager;
- private final ConnectivityManager mConnectivityManager;
- private TelephonyManager mTelephonyManager;
- private PhoneStateListener mPhoneStateListener;
- private WakeLock mProximityWakelock;
- private final SensorManager mSensorManager;
- private final Sensor mProximity;
- private final MediaScanner mMediaScanner;
- private Timer mTimer;
-
- private final LinphonePreferences mPrefs;
- private Core mCore;
- private CoreListenerStub mCoreListener;
- private AccountCreator mAccountCreator;
-
- private boolean mExited;
- private boolean mCallGsmON;
- private boolean mProximitySensingEnabled;
- private boolean mHasLastCallSasBeenRejected;
- private Runnable mIterateRunnable;
-
- public LinphoneManager(Context c) {
- mExited = false;
- mContext = c;
- mBasePath = c.getFilesDir().getAbsolutePath();
- mCallLogDatabaseFile = mBasePath + "/linphone-log-history.db";
- mFriendsDatabaseFile = mBasePath + "/linphone-friends.db";
- mRingSoundFile = mBasePath + "/share/sounds/linphone/rings/notes_of_the_optimistic.mkv";
- mUserCertsPath = mBasePath + "/user-certs";
-
- mPrefs = LinphonePreferences.instance();
- mPowerManager = (PowerManager) c.getSystemService(Context.POWER_SERVICE);
- mConnectivityManager =
- (ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE);
- mSensorManager = (SensorManager) c.getSystemService(Context.SENSOR_SERVICE);
- mProximity = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
- mTelephonyManager = (TelephonyManager) c.getSystemService(Context.TELEPHONY_SERVICE);
- mPhoneStateListener =
- new PhoneStateListener() {
- @Override
- public void onCallStateChanged(int state, String phoneNumber) {
- switch (state) {
- case TelephonyManager.CALL_STATE_OFFHOOK:
- Log.i("[Manager] Phone state is off hook");
- setCallGsmON(true);
- break;
- case TelephonyManager.CALL_STATE_RINGING:
- Log.i("[Manager] Phone state is ringing");
- setCallGsmON(true);
- break;
- case TelephonyManager.CALL_STATE_IDLE:
- Log.i("[Manager] Phone state is idle");
- setCallGsmON(false);
- break;
- }
- }
- };
-
- Log.i("[Manager] Registering phone state listener");
- mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
-
- mHasLastCallSasBeenRejected = false;
- mCallManager = new CallManager(c);
-
- File f = new File(mUserCertsPath);
- if (!f.exists()) {
- if (!f.mkdir()) {
- Log.e("[Manager] " + mUserCertsPath + " can't be created.");
- }
- }
-
- mMediaScanner = new MediaScanner(c);
-
- mCoreListener =
- new CoreListenerStub() {
- @SuppressLint("Wakelock")
- @Override
- public void onCallStateChanged(
- final Core core,
- final Call call,
- final State state,
- final String message) {
- Log.i("[Manager] Call state is [", state, "]");
- if (state == State.IncomingReceived
- && !call.equals(core.getCurrentCall())) {
- if (call.getReplacedCall() != null) {
- // attended transfer will be accepted automatically.
- return;
- }
- }
-
- if ((state == State.IncomingReceived || state == State.IncomingEarlyMedia)
- && getCallGsmON()) {
- if (mCore != null) {
- call.decline(Reason.Busy);
- }
- } else if (state == State.IncomingReceived
- && (LinphonePreferences.instance().isAutoAnswerEnabled())
- && !getCallGsmON()) {
- LinphoneUtils.dispatchOnUIThreadAfter(
- new Runnable() {
- @Override
- public void run() {
- if (mCore != null) {
- if (mCore.getCallsNb() > 0) {
- mCallManager.acceptCall(call);
- }
- }
- }
- },
- mPrefs.getAutoAnswerTime());
- } else if (state == State.End || state == State.Error) {
- if (mCore.getCallsNb() == 0) {
- // Disabling proximity sensor
- enableProximitySensing(false);
- }
- } else if (state == State.UpdatedByRemote) {
- // If the correspondent proposes video while audio call
- boolean remoteVideo = call.getRemoteParams().videoEnabled();
- boolean localVideo = call.getCurrentParams().videoEnabled();
- boolean autoAcceptCameraPolicy =
- LinphonePreferences.instance()
- .shouldAutomaticallyAcceptVideoRequests();
- if (remoteVideo
- && !localVideo
- && !autoAcceptCameraPolicy
- && mCore.getConference() == null) {
- call.deferUpdate();
- }
- }
- }
-
- @Override
- public void onFriendListCreated(Core core, FriendList list) {
- if (LinphoneContext.isReady()) {
- list.addListener(ContactsManager.getInstance());
- }
- }
-
- @Override
- public void onFriendListRemoved(Core core, FriendList list) {
- list.removeListener(ContactsManager.getInstance());
- }
- };
- }
-
- public static synchronized LinphoneManager getInstance() {
- LinphoneManager manager = LinphoneContext.instance().getLinphoneManager();
- if (manager == null) {
- throw new RuntimeException(
- "[Manager] Linphone Manager should be created before accessed");
- }
- if (manager.mExited) {
- throw new RuntimeException(
- "[Manager] Linphone Manager was already destroyed. "
- + "Better use getCore and check returned value");
- }
- return manager;
- }
-
- public static synchronized AndroidAudioManager getAudioManager() {
- return getInstance().mAudioManager;
- }
-
- public static synchronized CallManager getCallManager() {
- return getInstance().mCallManager;
- }
-
- public static synchronized Core getCore() {
- if (!LinphoneContext.isReady()) return null;
-
- if (getInstance().mExited) {
- // Can occur if the UI thread play a posted event but in the meantime the
- // LinphoneManager was destroyed
- // Ex: stop call and quickly terminate application.
- return null;
- }
- return getInstance().mCore;
- }
-
- /* End of static */
-
- public MediaScanner getMediaScanner() {
- return mMediaScanner;
- }
-
- public synchronized void destroy() {
- destroyManager();
- // Wait for Manager to destroy everything before setting mExited to true
- // Otherwise some objects might crash during their own destroy if they try to call
- // LinphoneManager.getCore(), for example to unregister a listener
- mExited = true;
- }
-
- public void restartCore() {
- Log.w("[Manager] Restarting Core");
- mCore.stop();
- mCore.start();
- }
-
- private void destroyCore() {
- Log.w("[Manager] Destroying Core");
- mCore.stop();
- mCore.removeListener(mCoreListener);
- }
-
- private synchronized void destroyManager() {
- Log.w("[Manager] Destroying Manager");
- changeStatusToOffline();
-
- if (mTelephonyManager != null) {
- Log.i("[Manager] Unregistering phone state listener");
- mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
- }
-
- if (mCallManager != null) mCallManager.destroy();
- if (mMediaScanner != null) mMediaScanner.destroy();
- if (mAudioManager != null) mAudioManager.destroy();
-
- if (mTimer != null) mTimer.cancel();
-
- if (mCore != null) {
- destroyCore();
- mCore = null;
- }
- }
-
- public synchronized void startLibLinphone(boolean isPush, CoreListener listener) {
- try {
- mCore =
- Factory.instance()
- .createCore(
- mPrefs.getLinphoneDefaultConfig(),
- mPrefs.getLinphoneFactoryConfig(),
- mContext);
- mCore.addListener(listener);
- mCore.addListener(mCoreListener);
-
- if (isPush) {
- Log.w(
- "[Manager] We are here because of a received push notification, enter background mode before starting the Core");
- mCore.enterBackground();
- }
-
- mCore.start();
-
- mIterateRunnable =
- new Runnable() {
- @Override
- public void run() {
- if (mCore != null) {
- mCore.iterate();
- }
- }
- };
- TimerTask lTask =
- new TimerTask() {
- @Override
- public void run() {
- LinphoneUtils.dispatchOnUIThread(mIterateRunnable);
- }
- };
- /*use schedule instead of scheduleAtFixedRate to avoid iterate from being call in burst after cpu wake up*/
- mTimer = new Timer("Linphone scheduler");
- mTimer.schedule(lTask, 0, 20);
-
- configureCore();
- } catch (Exception e) {
- Log.e(e, "[Manager] Cannot start linphone");
- }
- }
-
- private synchronized void configureCore() {
- Log.i("[Manager] Configuring Core");
- mAudioManager = new AndroidAudioManager(mContext);
-
- mCore.setZrtpSecretsFile(mBasePath + "/zrtp_secrets");
-
- String deviceName = mPrefs.getDeviceName(mContext);
- String appName = mContext.getResources().getString(R.string.user_agent);
- String androidVersion = org.linphone.BuildConfig.VERSION_NAME;
- String userAgent = appName + "/" + androidVersion + " (" + deviceName + ") LinphoneSDK";
-
- mCore.setUserAgent(
- userAgent,
- getString(R.string.linphone_sdk_version)
- + " ("
- + getString(R.string.linphone_sdk_branch)
- + ")");
-
- // mCore.setChatDatabasePath(mChatDatabaseFile);
- mCore.setCallLogsDatabasePath(mCallLogDatabaseFile);
- mCore.setFriendsDatabasePath(mFriendsDatabaseFile);
- mCore.setUserCertificatesPath(mUserCertsPath);
- // mCore.setCallErrorTone(Reason.NotFound, mErrorToneFile);
- enableDeviceRingtone(mPrefs.isDeviceRingtoneEnabled());
-
- int availableCores = Runtime.getRuntime().availableProcessors();
- Log.w("[Manager] MediaStreamer : " + availableCores + " cores detected and configured");
-
- mCore.migrateLogsFromRcToDb();
-
- // Migrate existing linphone accounts to have conference factory uri and LIME X3Dh url set
- String uri = getString(R.string.default_conference_factory_uri);
- for (ProxyConfig lpc : mCore.getProxyConfigList()) {
- if (lpc.getIdentityAddress().getDomain().equals(getString(R.string.default_domain))) {
- if (lpc.getConferenceFactoryUri() == null) {
- lpc.edit();
- Log.i(
- "[Manager] Setting conference factory on proxy config "
- + lpc.getIdentityAddress().asString()
- + " to default value: "
- + uri);
- lpc.setConferenceFactoryUri(uri);
- lpc.done();
- }
-
- if (mCore.limeX3DhAvailable()) {
- String url = mCore.getLimeX3DhServerUrl();
- if (url == null || url.isEmpty()) {
- url = getString(R.string.default_lime_x3dh_server_url);
- Log.i("[Manager] Setting LIME X3Dh server url to default value: " + url);
- mCore.setLimeX3DhServerUrl(url);
- }
- }
- }
- }
-
- if (mContext.getResources().getBoolean(R.bool.enable_push_id)) {
- PushNotificationUtils.init(mContext);
- }
-
- mProximityWakelock =
- mPowerManager.newWakeLock(
- PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
- mContext.getPackageName() + ";manager_proximity_sensor");
-
- resetCameraFromPreferences();
-
- mAccountCreator = mCore.createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl());
- mCallGsmON = false;
-
- Log.i("[Manager] Core configured");
- }
-
- public void resetCameraFromPreferences() {
- Core core = getCore();
- if (core == null) return;
-
- boolean useFrontCam = LinphonePreferences.instance().useFrontCam();
- String firstDevice = null;
- for (String camera : core.getVideoDevicesList()) {
- if (firstDevice == null) {
- firstDevice = camera;
- }
-
- if (useFrontCam) {
- if (camera.contains("Front")) {
- Log.i("[Manager] Found front facing camera: " + camera);
- core.setVideoDevice(camera);
- return;
- }
- }
- }
-
- Log.i("[Manager] Using first camera available: " + firstDevice);
- core.setVideoDevice(firstDevice);
- }
-
- /* Account linking */
-
- public AccountCreator getAccountCreator() {
- if (mAccountCreator == null) {
- Log.w("[Manager] Account creator shouldn't be null !");
- mAccountCreator =
- mCore.createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl());
- }
- return mAccountCreator;
- }
-
- /* Presence stuff */
-
- private boolean isPresenceModelActivitySet() {
- if (mCore != null) {
- return mCore.getPresenceModel() != null
- && mCore.getPresenceModel().getActivity() != null;
- }
- return false;
- }
-
- public void changeStatusToOnline() {
- if (mCore == null) return;
- PresenceModel model = mCore.createPresenceModel();
- model.setBasicStatus(PresenceBasicStatus.Open);
- mCore.setPresenceModel(model);
- }
-
- public void changeStatusToOnThePhone() {
- if (mCore == null) return;
-
- if (isPresenceModelActivitySet()
- && mCore.getPresenceModel().getActivity().getType()
- != PresenceActivity.Type.OnThePhone) {
- mCore.getPresenceModel().getActivity().setType(PresenceActivity.Type.OnThePhone);
- } else if (!isPresenceModelActivitySet()) {
- PresenceModel model =
- mCore.createPresenceModelWithActivity(PresenceActivity.Type.OnThePhone, null);
- mCore.setPresenceModel(model);
- }
- }
-
- private void changeStatusToOffline() {
- if (mCore != null) {
- PresenceModel model = mCore.getPresenceModel();
- model.setBasicStatus(PresenceBasicStatus.Closed);
- mCore.setPresenceModel(model);
- }
- }
-
- /* Tunnel stuff */
-
- public void initTunnelFromConf() {
- if (!mCore.tunnelAvailable()) return;
-
- NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
- Tunnel tunnel = mCore.getTunnel();
- tunnel.cleanServers();
- TunnelConfig config = mPrefs.getTunnelConfig();
- if (config.getHost() != null) {
- tunnel.addServer(config);
- manageTunnelServer(info);
- }
- }
-
- private boolean isTunnelNeeded(NetworkInfo info) {
- if (info == null) {
- Log.i("[Manager] No connectivity: tunnel should be disabled");
- return false;
- }
-
- String pref = mPrefs.getTunnelMode();
-
- if (getString(R.string.tunnel_mode_entry_value_always).equals(pref)) {
- return true;
- }
-
- if (info.getType() != ConnectivityManager.TYPE_WIFI
- && getString(R.string.tunnel_mode_entry_value_3G_only).equals(pref)) {
- Log.i("[Manager] Need tunnel: 'no wifi' connection");
- return true;
- }
-
- return false;
- }
-
- private void manageTunnelServer(NetworkInfo info) {
- if (mCore == null) return;
- if (!mCore.tunnelAvailable()) return;
- Tunnel tunnel = mCore.getTunnel();
-
- Log.i("[Manager] Managing tunnel");
- if (isTunnelNeeded(info)) {
- Log.i("[Manager] Tunnel need to be activated");
- tunnel.setMode(Tunnel.Mode.Enable);
- } else {
- Log.i("[Manager] Tunnel should not be used");
- String pref = mPrefs.getTunnelMode();
- tunnel.setMode(Tunnel.Mode.Disable);
- if (getString(R.string.tunnel_mode_entry_value_auto).equals(pref)) {
- tunnel.setMode(Tunnel.Mode.Auto);
- }
- }
- }
-
- /* Proximity sensor stuff */
-
- public void enableProximitySensing(boolean enable) {
- if (enable) {
- if (!mProximitySensingEnabled) {
- mSensorManager.registerListener(
- this, mProximity, SensorManager.SENSOR_DELAY_NORMAL);
- mProximitySensingEnabled = true;
- }
- } else {
- if (mProximitySensingEnabled) {
- mSensorManager.unregisterListener(this);
- mProximitySensingEnabled = false;
- // Don't forgeting to release wakelock if held
- if (mProximityWakelock.isHeld()) {
- mProximityWakelock.release();
- }
- }
- }
- }
-
- private Boolean isProximitySensorNearby(final SensorEvent event) {
- float threshold = 4.001f; // <= 4 cm is near
-
- final float distanceInCm = event.values[0];
- final float maxDistance = event.sensor.getMaximumRange();
- Log.d(
- "[Manager] Proximity sensor report ["
- + distanceInCm
- + "] , for max range ["
- + maxDistance
- + "]");
-
- if (maxDistance <= threshold) {
- // Case binary 0/1 and short sensors
- threshold = maxDistance;
- }
- return distanceInCm < threshold;
- }
-
- @Override
- public void onSensorChanged(SensorEvent event) {
- if (event.timestamp == 0) return;
- if (isProximitySensorNearby(event)) {
- if (!mProximityWakelock.isHeld()) {
- mProximityWakelock.acquire();
- }
- } else {
- if (mProximityWakelock.isHeld()) {
- mProximityWakelock.release();
- }
- }
- }
-
- @Override
- public void onAccuracyChanged(Sensor sensor, int accuracy) {}
-
- /* Other stuff */
-
- public void enableDeviceRingtone(boolean use) {
- if (use) {
- mCore.setRing(null);
- } else {
- mCore.setRing(mRingSoundFile);
- }
- }
-
- public boolean getCallGsmON() {
- return mCallGsmON;
- }
-
- public void setCallGsmON(boolean on) {
- mCallGsmON = on;
- if (on && mCore != null) {
- mCore.pauseAllCalls();
- }
- }
-
- private String getString(int key) {
- return mContext.getString(key);
- }
-
- public boolean hasLastCallSasBeenRejected() {
- return mHasLastCallSasBeenRejected;
- }
-
- public void lastCallSasRejected(boolean rejected) {
- mHasLastCallSasBeenRejected = rejected;
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/AboutActivity.java b/app/src/main/java/org/linphone/activities/AboutActivity.java
deleted file mode 100644
index b4ff91423..000000000
--- a/app/src/main/java/org/linphone/activities/AboutActivity.java
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for 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.app.ProgressDialog;
-import android.content.Intent;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-import androidx.core.content.ContextCompat;
-import org.linphone.LinphoneManager;
-import org.linphone.R;
-import org.linphone.core.Core;
-import org.linphone.core.CoreListenerStub;
-import org.linphone.settings.LinphonePreferences;
-
-public class AboutActivity extends MainActivity {
- private CoreListenerStub mListener;
- private ProgressDialog mProgress;
- private boolean mUploadInProgress;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- mOnBackPressGoHome = false;
- mAlwaysHideTabBar = true;
-
- // Uses the fragment container layout to inflate the about view instead of using a fragment
- View aboutView = LayoutInflater.from(this).inflate(R.layout.about, null, false);
- LinearLayout fragmentContainer = findViewById(R.id.fragmentContainer);
- LinearLayout.LayoutParams params =
- new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
- fragmentContainer.addView(aboutView, params);
-
- if (isTablet()) {
- findViewById(R.id.fragmentContainer2).setVisibility(View.GONE);
- }
-
- TextView aboutVersion = findViewById(R.id.about_android_version);
- TextView aboutLiblinphoneVersion = findViewById(R.id.about_liblinphone_sdk_version);
- aboutLiblinphoneVersion.setText(
- String.format(
- getString(R.string.about_liblinphone_sdk_version),
- getString(R.string.linphone_sdk_version)
- + " ("
- + getString(R.string.linphone_sdk_branch)
- + ")"));
- // We can't access a library's BuildConfig, so we have to set it as a resource
- aboutVersion.setText(
- String.format(
- getString(R.string.about_version),
- org.linphone.BuildConfig.VERSION_NAME
- + " ("
- + org.linphone.BuildConfig.BUILD_TYPE
- + ")"));
-
- TextView privacyPolicy = findViewById(R.id.privacy_policy_link);
- privacyPolicy.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent browserIntent =
- new Intent(
- Intent.ACTION_VIEW,
- Uri.parse(getString(R.string.about_privacy_policy_link)));
- startActivity(browserIntent);
- }
- });
-
- TextView license = findViewById(R.id.about_text);
- license.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent browserIntent =
- new Intent(
- Intent.ACTION_VIEW,
- Uri.parse(getString(R.string.about_license_link)));
- startActivity(browserIntent);
- }
- });
-
- Button sendLogs = findViewById(R.id.send_log);
- sendLogs.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Core core = LinphoneManager.getCore();
- if (core != null) {
- core.uploadLogCollection();
- }
- }
- });
- sendLogs.setVisibility(
- LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE);
-
- Button resetLogs = findViewById(R.id.reset_log);
- resetLogs.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Core core = LinphoneManager.getCore();
- if (core != null) {
- core.resetLogCollection();
- }
- }
- });
- resetLogs.setVisibility(
- LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE);
-
- mListener =
- new CoreListenerStub() {
- @Override
- public void onLogCollectionUploadProgressIndication(
- Core core, int offset, int total) {}
-
- @Override
- public void onLogCollectionUploadStateChanged(
- Core core, Core.LogCollectionUploadState state, String info) {
- if (state == Core.LogCollectionUploadState.InProgress) {
- displayUploadLogsInProgress();
- } else if (state == Core.LogCollectionUploadState.Delivered
- || state == Core.LogCollectionUploadState.NotDelivered) {
- mUploadInProgress = false;
- if (mProgress != null) mProgress.dismiss();
- }
- }
- };
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- showTopBarWithTitle(getString(R.string.about));
- if (getResources().getBoolean(R.bool.hide_bottom_bar_on_second_level_views)) {
- hideTabBar();
- }
-
- Core core = LinphoneManager.getCore();
- if (core != null) {
- core.addListener(mListener);
- }
- }
-
- @Override
- public void onPause() {
- Core core = LinphoneManager.getCore();
- if (core != null) {
- core.removeListener(mListener);
- }
-
- super.onPause();
- }
-
- @Override
- protected void onDestroy() {
- mListener = null;
- mProgress = null;
-
- super.onDestroy();
- }
-
- private void displayUploadLogsInProgress() {
- if (mUploadInProgress) {
- return;
- }
- mUploadInProgress = true;
-
- mProgress = ProgressDialog.show(this, null, null);
- Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.light_grey_color));
- d.setAlpha(200);
- mProgress
- .getWindow()
- .setLayout(
- WindowManager.LayoutParams.MATCH_PARENT,
- WindowManager.LayoutParams.MATCH_PARENT);
- mProgress.getWindow().setBackgroundDrawable(d);
- mProgress.setContentView(R.layout.wait_layout);
- mProgress.show();
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/GenericActivity.kt b/app/src/main/java/org/linphone/activities/GenericActivity.kt
new file mode 100644
index 000000000..710dde4ad
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/GenericActivity.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.view.Surface
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.app.AppCompatDelegate
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.LinphoneApplication.Companion.corePreferences
+import org.linphone.R
+import org.linphone.core.tools.Log
+
+abstract class GenericActivity : AppCompatActivity() {
+ @SuppressLint("SourceLockedOrientationActivity")
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (corePreferences.forcePortrait) {
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+ }
+
+ 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
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
+ }
+ }
+ Configuration.UI_MODE_NIGHT_YES -> {
+ if (darkModeEnabled == 0) {
+ // Force light mode
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
+ }
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ var degrees = 270
+ val orientation = windowManager.defaultDisplay.rotation
+ when (orientation) {
+ Surface.ROTATION_0 -> degrees = 0
+ Surface.ROTATION_90 -> degrees = 270
+ Surface.ROTATION_180 -> degrees = 180
+ Surface.ROTATION_270 -> degrees = 90
+ }
+ Log.i("[Generic Activity] Device orientation is $degrees (raw value is $orientation)")
+ val rotation = (360 - degrees) % 360
+ coreContext.core.deviceRotation = rotation
+
+ // Remove service notification if it has been started by device boot
+ coreContext.notificationsManager.stopForegroundNotificationIfPossible()
+ }
+
+ fun isTablet(): Boolean {
+ return resources.getBoolean(R.bool.isTablet)
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/LinphoneGenericActivity.java b/app/src/main/java/org/linphone/activities/LinphoneGenericActivity.java
deleted file mode 100644
index 100fe0c41..000000000
--- a/app/src/main/java/org/linphone/activities/LinphoneGenericActivity.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for 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.Intent;
-import android.os.Bundle;
-import android.view.Surface;
-import org.linphone.LinphoneContext;
-import org.linphone.LinphoneManager;
-import org.linphone.core.Core;
-import org.linphone.core.tools.Log;
-import org.linphone.service.LinphoneService;
-
-public abstract class LinphoneGenericActivity extends ThemeableActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- ensureServiceIsRunning();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- ensureServiceIsRunning();
-
- if (LinphoneContext.isReady()) {
- int degrees = 270;
- int orientation = getWindowManager().getDefaultDisplay().getRotation();
- switch (orientation) {
- case Surface.ROTATION_0:
- degrees = 0;
- break;
- case Surface.ROTATION_90:
- degrees = 270;
- break;
- case Surface.ROTATION_180:
- degrees = 180;
- break;
- case Surface.ROTATION_270:
- degrees = 90;
- break;
- }
-
- Log.i(
- "[Generic Activity] Device orientation is "
- + degrees
- + " (raw value is "
- + orientation
- + ")");
-
- int rotation = (360 - degrees) % 360;
- Core core = LinphoneManager.getCore();
- if (core != null) {
- core.setDeviceRotation(rotation);
- }
- }
- }
-
- private void ensureServiceIsRunning() {
- if (!LinphoneService.isReady()) {
- if (!LinphoneContext.isReady()) {
- new LinphoneContext(getApplicationContext());
- LinphoneContext.instance().start(false);
- Log.i("[Generic Activity] Context created & started");
- }
-
- Log.i("[Generic Activity] Starting Service");
- try {
- startService(new Intent().setClass(this, LinphoneService.class));
- } catch (IllegalStateException ise) {
- Log.e("[Generic Activity] Couldn't start service, exception: ", ise);
- }
- }
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/LinphoneLauncherActivity.java b/app/src/main/java/org/linphone/activities/LinphoneLauncherActivity.java
deleted file mode 100644
index 3ab8d80ad..000000000
--- a/app/src/main/java/org/linphone/activities/LinphoneLauncherActivity.java
+++ /dev/null
@@ -1,109 +0,0 @@
-/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for 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.app.Activity;
-import android.content.Intent;
-import android.content.pm.ActivityInfo;
-import android.os.Bundle;
-import android.util.Log;
-import org.linphone.LinphoneManager;
-import org.linphone.R;
-import org.linphone.assistant.MenuAssistantActivity;
-import org.linphone.chat.ChatActivity;
-import org.linphone.contacts.ContactsActivity;
-import org.linphone.dialer.DialerActivity;
-import org.linphone.history.HistoryActivity;
-import org.linphone.service.LinphoneService;
-import org.linphone.service.ServiceWaitThread;
-import org.linphone.service.ServiceWaitThreadListener;
-import org.linphone.settings.LinphonePreferences;
-
-/** Creates LinphoneService and wait until Core is ready to start main Activity */
-public class LinphoneLauncherActivity extends Activity implements ServiceWaitThreadListener {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- if (getResources().getBoolean(R.bool.orientation_portrait_only)) {
- setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- }
-
- if (!getResources().getBoolean(R.bool.use_full_screen_image_splashscreen)) {
- setContentView(R.layout.launch_screen);
- } // Otherwise use drawable/launch_screen layer list up until first activity starts
- }
-
- @Override
- protected void onStart() {
- super.onStart();
-
- if (LinphoneService.isReady()) {
- onServiceReady();
- } else {
- try {
- startService(
- new Intent()
- .setClass(LinphoneLauncherActivity.this, LinphoneService.class));
- new ServiceWaitThread(this).start();
- } catch (IllegalStateException ise) {
- Log.e("Linphone", "Exception raised while starting service: " + ise);
- }
- }
- }
-
- @Override
- public void onServiceReady() {
- final Class extends Activity> classToStart;
-
- boolean useFirstLoginActivity =
- getResources().getBoolean(R.bool.display_account_assistant_at_first_start);
- if (useFirstLoginActivity && LinphonePreferences.instance().isFirstLaunch()) {
- classToStart = MenuAssistantActivity.class;
- } else {
- if (getIntent().getExtras() != null) {
- String activity = getIntent().getExtras().getString("Activity", null);
- if (ChatActivity.NAME.equals(activity)) {
- classToStart = ChatActivity.class;
- } else if (HistoryActivity.NAME.equals(activity)) {
- classToStart = HistoryActivity.class;
- } else if (ContactsActivity.NAME.equals(activity)) {
- classToStart = ContactsActivity.class;
- } else {
- classToStart = DialerActivity.class;
- }
- } else {
- classToStart = DialerActivity.class;
- }
- }
-
- Intent intent = new Intent();
- intent.setClass(LinphoneLauncherActivity.this, classToStart);
- if (getIntent() != null && getIntent().getExtras() != null) {
- intent.putExtras(getIntent().getExtras());
- }
- intent.setAction(getIntent().getAction());
- intent.setType(getIntent().getType());
- intent.setData(getIntent().getData());
- startActivity(intent);
-
- LinphoneManager.getInstance().changeStatusToOnline();
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/MainActivity.java b/app/src/main/java/org/linphone/activities/MainActivity.java
deleted file mode 100644
index bd5091699..000000000
--- a/app/src/main/java/org/linphone/activities/MainActivity.java
+++ /dev/null
@@ -1,968 +0,0 @@
-/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for 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.Manifest;
-import android.app.Dialog;
-import android.app.Fragment;
-import android.app.FragmentManager;
-import android.app.FragmentTransaction;
-import android.app.KeyguardManager;
-import android.content.ActivityNotFoundException;
-import android.content.ClipData;
-import android.content.ClipboardManager;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.os.Bundle;
-import android.view.KeyEvent;
-import android.view.View;
-import android.widget.Button;
-import android.widget.CheckBox;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-import android.widget.Toast;
-import androidx.core.app.ActivityCompat;
-import androidx.drawerlayout.widget.DrawerLayout;
-import java.sql.Timestamp;
-import java.util.ArrayList;
-import java.util.Date;
-import org.linphone.LinphoneContext;
-import org.linphone.LinphoneManager;
-import org.linphone.R;
-import org.linphone.assistant.PhoneAccountLinkingAssistantActivity;
-import org.linphone.call.CallActivity;
-import org.linphone.call.CallIncomingActivity;
-import org.linphone.call.CallOutgoingActivity;
-import org.linphone.chat.ChatActivity;
-import org.linphone.compatibility.Compatibility;
-import org.linphone.contacts.ContactsActivity;
-import org.linphone.contacts.ContactsManager;
-import org.linphone.contacts.LinphoneContact;
-import org.linphone.core.AccountCreator;
-import org.linphone.core.AccountCreatorListenerStub;
-import org.linphone.core.Address;
-import org.linphone.core.Call;
-import org.linphone.core.ChatMessage;
-import org.linphone.core.ChatRoom;
-import org.linphone.core.Core;
-import org.linphone.core.CoreListenerStub;
-import org.linphone.core.ProxyConfig;
-import org.linphone.core.RegistrationState;
-import org.linphone.core.tools.Log;
-import org.linphone.dialer.DialerActivity;
-import org.linphone.fragments.EmptyFragment;
-import org.linphone.fragments.StatusBarFragment;
-import org.linphone.history.HistoryActivity;
-import org.linphone.menu.SideMenuFragment;
-import org.linphone.service.LinphoneService;
-import org.linphone.settings.LinphonePreferences;
-import org.linphone.settings.SettingsActivity;
-import org.linphone.utils.DeviceUtils;
-import org.linphone.utils.LinphoneUtils;
-
-public abstract class MainActivity extends LinphoneGenericActivity
- implements StatusBarFragment.MenuClikedListener, SideMenuFragment.QuitClikedListener {
- private static final int MAIN_PERMISSIONS = 1;
- protected static final int FRAGMENT_SPECIFIC_PERMISSION = 2;
-
- private TextView mMissedCalls;
- private TextView mMissedMessages;
- protected View mContactsSelected;
- protected View mHistorySelected;
- protected View mDialerSelected;
- protected View mChatSelected;
- private LinearLayout mTopBar;
- private TextView mTopBarTitle;
- private LinearLayout mTabBar;
-
- private SideMenuFragment mSideMenuFragment;
- private StatusBarFragment mStatusBarFragment;
-
- protected boolean mOnBackPressGoHome;
- protected boolean mAlwaysHideTabBar;
- protected String[] mPermissionsToHave;
-
- private CoreListenerStub mListener;
- private AccountCreatorListenerStub mAccountCreatorListener;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.main);
-
- mOnBackPressGoHome = true;
- mAlwaysHideTabBar = false;
-
- RelativeLayout history = findViewById(R.id.history);
- history.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(MainActivity.this, HistoryActivity.class);
- addFlagsToIntent(intent);
- startActivity(intent);
- }
- });
- RelativeLayout contacts = findViewById(R.id.contacts);
- contacts.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(MainActivity.this, ContactsActivity.class);
- addFlagsToIntent(intent);
- startActivity(intent);
- }
- });
- RelativeLayout dialer = findViewById(R.id.dialer);
- dialer.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(MainActivity.this, DialerActivity.class);
- addFlagsToIntent(intent);
- startActivity(intent);
- }
- });
- RelativeLayout chat = findViewById(R.id.chat);
- chat.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- Intent intent = new Intent(MainActivity.this, ChatActivity.class);
- addFlagsToIntent(intent);
- startActivity(intent);
- }
- });
-
- mMissedCalls = findViewById(R.id.missed_calls);
- mMissedMessages = findViewById(R.id.missed_chats);
-
- mHistorySelected = findViewById(R.id.history_select);
- mContactsSelected = findViewById(R.id.contacts_select);
- mDialerSelected = findViewById(R.id.dialer_select);
- mChatSelected = findViewById(R.id.chat_select);
-
- mTabBar = findViewById(R.id.footer);
- mTopBar = findViewById(R.id.top_bar);
- mTopBarTitle = findViewById(R.id.top_bar_title);
-
- ImageView back = findViewById(R.id.cancel);
- back.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- goBack();
- }
- });
-
- mStatusBarFragment =
- (StatusBarFragment) getFragmentManager().findFragmentById(R.id.status_fragment);
-
- DrawerLayout mSideMenu = findViewById(R.id.side_menu);
- RelativeLayout mSideMenuContent = findViewById(R.id.side_menu_content);
- mSideMenuFragment =
- (SideMenuFragment)
- getSupportFragmentManager().findFragmentById(R.id.side_menu_fragment);
- mSideMenuFragment.setDrawer(mSideMenu, mSideMenuContent);
-
- if (getResources().getBoolean(R.bool.disable_chat)) {
- chat.setVisibility(View.GONE);
- }
-
- mListener =
- new CoreListenerStub() {
- @Override
- public void onCallStateChanged(
- Core core, Call call, Call.State state, String message) {
- if (state == Call.State.End || state == Call.State.Released) {
- displayMissedCalls();
- }
- }
-
- @Override
- public void onMessageReceived(Core core, ChatRoom room, ChatMessage message) {
- displayMissedChats();
- }
-
- @Override
- public void onChatRoomRead(Core core, ChatRoom room) {
- displayMissedChats();
- }
-
- @Override
- public void onMessageReceivedUnableDecrypt(
- Core core, ChatRoom room, ChatMessage message) {
- displayMissedChats();
- }
-
- @Override
- public void onRegistrationStateChanged(
- Core core,
- ProxyConfig proxyConfig,
- RegistrationState state,
- String message) {
- mSideMenuFragment.displayAccountsInSideMenu();
-
- if (state == RegistrationState.Ok) {
- // For push notifications to work on some devices,
- // app must be in "protected mode" in battery settings...
- // https://stackoverflow.com/questions/31638986/protected-apps-setting-on-huawei-phones-and-how-to-handle-it
- DeviceUtils
- .displayDialogIfDeviceHasPowerManagerThatCouldPreventPushNotifications(
- MainActivity.this);
-
- if (getResources().getBoolean(R.bool.use_phone_number_validation)) {
- if (proxyConfig
- .getDomain()
- .equals(getString(R.string.default_domain))) {
- isAccountWithAlias();
- }
- }
-
- if (!Compatibility.isDoNotDisturbSettingsAccessGranted(
- MainActivity.this)) {
- displayDNDSettingsDialog();
- }
- }
- }
-
- @Override
- public void onLogCollectionUploadStateChanged(
- Core core, Core.LogCollectionUploadState state, String info) {
- Log.d(
- "[Main Activity] Log upload state: "
- + state.toString()
- + ", info = "
- + info);
- if (state == Core.LogCollectionUploadState.Delivered) {
- ClipboardManager clipboard =
- (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
- ClipData clip = ClipData.newPlainText("Logs url", info);
- clipboard.setPrimaryClip(clip);
- Toast.makeText(
- MainActivity.this,
- getString(R.string.logs_url_copied_to_clipboard),
- Toast.LENGTH_SHORT)
- .show();
- shareUploadedLogsUrl(info);
- }
- }
- };
-
- mAccountCreatorListener =
- new AccountCreatorListenerStub() {
- @Override
- public void onIsAccountExist(
- AccountCreator accountCreator,
- AccountCreator.Status status,
- String resp) {
- if (status.equals(AccountCreator.Status.AccountExist)) {
- accountCreator.isAccountLinked();
- }
- }
-
- @Override
- public void onLinkAccount(
- AccountCreator accountCreator,
- AccountCreator.Status status,
- String resp) {
- if (status.equals(AccountCreator.Status.AccountNotLinked)) {
- askLinkWithPhoneNumber();
- }
- }
-
- @Override
- public void onIsAccountLinked(
- AccountCreator accountCreator,
- AccountCreator.Status status,
- String resp) {
- if (status.equals(AccountCreator.Status.AccountNotLinked)) {
- askLinkWithPhoneNumber();
- }
- }
- };
- }
-
- @Override
- protected void onStart() {
- super.onStart();
-
- requestRequiredPermissions();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- LinphoneContext.instance()
- .getNotificationManager()
- .removeForegroundServiceNotificationIfPossible();
-
- hideTopBar();
- if (!mAlwaysHideTabBar
- && (getFragmentManager().getBackStackEntryCount() == 0
- || !getResources()
- .getBoolean(R.bool.hide_bottom_bar_on_second_level_views))) {
- showTabBar();
- }
-
- mHistorySelected.setVisibility(View.GONE);
- mContactsSelected.setVisibility(View.GONE);
- mDialerSelected.setVisibility(View.GONE);
- mChatSelected.setVisibility(View.GONE);
-
- mStatusBarFragment.setMenuListener(this);
- mSideMenuFragment.setQuitListener(this);
- mSideMenuFragment.displayAccountsInSideMenu();
-
- if (mSideMenuFragment.isOpened()) {
- mSideMenuFragment.closeDrawer();
- }
-
- Core core = LinphoneManager.getCore();
- if (core != null) {
- core.addListener(mListener);
- displayMissedChats();
- displayMissedCalls();
- }
- }
-
- @Override
- protected void onPause() {
- mStatusBarFragment.setMenuListener(null);
- mSideMenuFragment.setQuitListener(null);
-
- Core core = LinphoneManager.getCore();
- if (core != null) {
- core.removeListener(mListener);
- }
-
- super.onPause();
- }
-
- @Override
- protected void onDestroy() {
- mMissedCalls = null;
- mMissedMessages = null;
- mContactsSelected = null;
- mHistorySelected = null;
- mDialerSelected = null;
- mChatSelected = null;
- mTopBar = null;
- mTopBarTitle = null;
- mTabBar = null;
-
- mSideMenuFragment = null;
- mStatusBarFragment = null;
-
- mListener = null;
-
- super.onDestroy();
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- try {
- super.onSaveInstanceState(outState);
- } catch (IllegalStateException ise) {
- // Do not log this exception
- }
- }
-
- @Override
- protected void onRestoreInstanceState(Bundle savedInstanceState) {
- try {
- super.onRestoreInstanceState(savedInstanceState);
- } catch (IllegalStateException ise) {
- // Do not log this exception
- }
- }
-
- @Override
- public void onMenuCliked() {
- if (mSideMenuFragment.isOpened()) {
- mSideMenuFragment.openOrCloseSideMenu(false, true);
- } else {
- mSideMenuFragment.openOrCloseSideMenu(true, true);
- }
- }
-
- @Override
- public void onQuitClicked() {
- quit();
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- if (keyCode == KeyEvent.KEYCODE_BACK) {
- if (mOnBackPressGoHome) {
- if (getFragmentManager().getBackStackEntryCount() == 0) {
- goHomeAndClearStack();
- return true;
- }
- }
- goBack();
- return true;
- }
- return super.onKeyDown(keyCode, event);
- }
-
- public boolean popBackStack() {
- if (getFragmentManager().getBackStackEntryCount() > 0) {
- getFragmentManager().popBackStackImmediate();
- if (!mAlwaysHideTabBar
- && (getFragmentManager().getBackStackEntryCount() == 0
- && getResources()
- .getBoolean(R.bool.hide_bottom_bar_on_second_level_views))) {
- showTabBar();
- }
- return true;
- }
- return false;
- }
-
- public void goBack() {
- finish();
- }
-
- protected boolean isTablet() {
- return getResources().getBoolean(R.bool.isTablet);
- }
-
- private void goHomeAndClearStack() {
- Intent intent = new Intent();
- intent.setAction(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
- try {
- startActivity(intent);
- } catch (IllegalStateException ise) {
- Log.e("[Main Activity] Can't start home activity: ", ise);
- }
- }
-
- private void quit() {
- goHomeAndClearStack();
- if (LinphoneService.isReady()
- && LinphonePreferences.instance().getServiceNotificationVisibility()) {
- LinphoneService.instance().stopSelf();
- }
- }
-
- // Tab, Top and Status bars
-
- public void hideStatusBar() {
- findViewById(R.id.status_fragment).setVisibility(View.GONE);
- }
-
- public void showStatusBar() {
- findViewById(R.id.status_fragment).setVisibility(View.VISIBLE);
- }
-
- public void hideTabBar() {
- if (!isTablet()) { // do not hide if tablet, otherwise won't be able to navigate...
- mTabBar.setVisibility(View.GONE);
- }
- }
-
- public void showTabBar() {
- mTabBar.setVisibility(View.VISIBLE);
- }
-
- protected void hideTopBar() {
- mTopBar.setVisibility(View.GONE);
- mTopBarTitle.setText("");
- }
-
- private void showTopBar() {
- mTopBar.setVisibility(View.VISIBLE);
- }
-
- protected void showTopBarWithTitle(String title) {
- showTopBar();
- mTopBarTitle.setText(title);
- }
-
- // Permissions
-
- public boolean checkPermission(String permission) {
- int granted = getPackageManager().checkPermission(permission, getPackageName());
- Log.i(
- "[Permission] "
- + permission
- + " permission is "
- + (granted == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
- return granted == PackageManager.PERMISSION_GRANTED;
- }
-
- public boolean checkPermissions(String[] permissions) {
- boolean allGranted = true;
- for (String permission : permissions) {
- allGranted &= checkPermission(permission);
- }
- return allGranted;
- }
-
- public void requestPermissionIfNotGranted(String permission) {
- if (!checkPermission(permission)) {
- Log.i("[Permission] Requesting " + permission + " permission");
-
- String[] permissions = new String[] {permission};
- KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
- boolean locked = km.inKeyguardRestrictedInputMode();
- if (!locked) {
- // This is to workaround an infinite loop of pause/start in Activity issue
- // if incoming call ends while screen if off and locked
- ActivityCompat.requestPermissions(this, permissions, FRAGMENT_SPECIFIC_PERMISSION);
- }
- }
- }
-
- public void requestPermissionsIfNotGranted(String[] perms) {
- requestPermissionsIfNotGranted(perms, FRAGMENT_SPECIFIC_PERMISSION);
- }
-
- private void requestPermissionsIfNotGranted(String[] perms, int resultCode) {
- ArrayList permissionsToAskFor = new ArrayList<>();
- if (perms != null) { // This is created (or not) by the child activity
- for (String permissionToHave : perms) {
- if (!checkPermission(permissionToHave)) {
- permissionsToAskFor.add(permissionToHave);
- }
- }
- }
-
- if (permissionsToAskFor.size() > 0) {
- for (String permission : permissionsToAskFor) {
- Log.i("[Permission] Requesting " + permission + " permission");
- }
- String[] permissions = new String[permissionsToAskFor.size()];
- permissions = permissionsToAskFor.toArray(permissions);
-
- KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
- boolean locked = km.inKeyguardRestrictedInputMode();
- if (!locked) {
- // This is to workaround an infinite loop of pause/start in Activity issue
- // if incoming call ends while screen if off and locked
- ActivityCompat.requestPermissions(this, permissions, resultCode);
- }
- }
- }
-
- private void requestRequiredPermissions() {
- requestPermissionsIfNotGranted(mPermissionsToHave, MAIN_PERMISSIONS);
- }
-
- @Override
- public void onRequestPermissionsResult(
- int requestCode, String[] permissions, int[] grantResults) {
- if (permissions.length <= 0) return;
-
- for (int i = 0; i < permissions.length; i++) {
- Log.i(
- "[Permission] "
- + permissions[i]
- + " is "
- + (grantResults[i] == PackageManager.PERMISSION_GRANTED
- ? "granted"
- : "denied"));
- if (permissions[i].equals(Manifest.permission.READ_CONTACTS)
- || permissions[i].equals(Manifest.permission.WRITE_CONTACTS)) {
- if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
- if (LinphoneContext.isReady()) {
- ContactsManager.getInstance().enableContactsAccess();
- ContactsManager.getInstance().initializeContactManager();
- ContactsManager.getInstance().fetchContactsAsync();
- }
- }
- } else if (permissions[i].equals(Manifest.permission.READ_EXTERNAL_STORAGE)) {
- boolean enableRingtone = grantResults[i] == PackageManager.PERMISSION_GRANTED;
- LinphonePreferences.instance().enableDeviceRingtone(enableRingtone);
- LinphoneManager.getInstance().enableDeviceRingtone(enableRingtone);
- } else if (permissions[i].equals(Manifest.permission.CAMERA)
- && grantResults[i] == PackageManager.PERMISSION_GRANTED) {
- LinphoneUtils.reloadVideoDevices();
- }
- }
- }
-
- // Missed calls & chat indicators
-
- protected void displayMissedCalls() {
- int count = 0;
- Core core = LinphoneManager.getCore();
- if (core != null) {
- count = core.getMissedCallsCount();
- }
-
- if (count > 0) {
- mMissedCalls.setText(String.valueOf(count));
- mMissedCalls.setVisibility(View.VISIBLE);
- } else {
- mMissedCalls.clearAnimation();
- mMissedCalls.setVisibility(View.GONE);
- }
- }
-
- public void displayMissedChats() {
- int count = 0;
- Core core = LinphoneManager.getCore();
- if (core != null) {
- count = core.getUnreadChatMessageCountFromActiveLocals();
- }
-
- if (count > 0) {
- mMissedMessages.setText(String.valueOf(count));
- mMissedMessages.setVisibility(View.VISIBLE);
- } else {
- mMissedMessages.clearAnimation();
- mMissedMessages.setVisibility(View.GONE);
- }
- }
-
- // Navigation between actvities
-
- public void goBackToCall() {
- boolean incoming = false;
- boolean outgoing = false;
- Call[] calls = LinphoneManager.getCore().getCalls();
-
- for (Call call : calls) {
- Call.State state = call.getState();
- switch (state) {
- case IncomingEarlyMedia:
- case IncomingReceived:
- incoming = true;
- break;
- case OutgoingEarlyMedia:
- case OutgoingInit:
- case OutgoingProgress:
- case OutgoingRinging:
- outgoing = true;
- break;
- }
- }
-
- if (incoming) {
- startActivity(new Intent(this, CallIncomingActivity.class));
- } else if (outgoing) {
- startActivity(new Intent(this, CallOutgoingActivity.class));
- } else {
- startActivity(new Intent(this, CallActivity.class));
- }
- }
-
- public void newOutgoingCall(String to) {
- if (LinphoneManager.getCore().getCallsNb() > 0) {
- Intent intent = new Intent(this, DialerActivity.class);
- intent.addFlags(
- Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
- intent.putExtra("SipUri", to);
- this.startActivity(intent);
- } else {
- LinphoneManager.getCallManager().newOutgoingCall(to, null);
- }
- }
-
- private void addFlagsToIntent(Intent intent) {
- intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
- }
-
- protected void changeFragment(Fragment fragment, String name, boolean isChild) {
- FragmentManager fragmentManager = getFragmentManager();
- FragmentTransaction transaction = fragmentManager.beginTransaction();
-
- if (transaction.isAddToBackStackAllowed()) {
- int count = fragmentManager.getBackStackEntryCount();
- if (count > 0) {
- FragmentManager.BackStackEntry entry =
- fragmentManager.getBackStackEntryAt(count - 1);
-
- if (entry != null && name.equals(entry.getName())) {
- fragmentManager.popBackStack();
- if (!isChild) {
- // We just removed it's duplicate from the back stack
- // And we want at least one in it
- transaction.addToBackStack(name);
- }
- }
- }
-
- if (isChild) {
- transaction.addToBackStack(name);
- }
- }
-
- if (getResources().getBoolean(R.bool.hide_bottom_bar_on_second_level_views)) {
- if (isChild) {
- if (!isTablet()) {
- hideTabBar();
- }
- } else {
- showTabBar();
- }
- }
-
- Compatibility.setFragmentTransactionReorderingAllowed(transaction, false);
- if (isChild && isTablet()) {
- transaction.replace(R.id.fragmentContainer2, fragment, name);
- findViewById(R.id.fragmentContainer2).setVisibility(View.VISIBLE);
- } else {
- transaction.replace(R.id.fragmentContainer, fragment, name);
- }
- transaction.commitAllowingStateLoss();
- fragmentManager.executePendingTransactions();
- }
-
- public void showEmptyChildFragment() {
- changeFragment(new EmptyFragment(), "Empty", true);
- }
-
- public void showAccountSettings(int accountIndex) {
- Intent intent = new Intent(this, SettingsActivity.class);
- addFlagsToIntent(intent);
- intent.putExtra("Account", accountIndex);
- startActivity(intent);
- }
-
- public void showContactDetails(LinphoneContact contact) {
- Intent intent = new Intent(this, ContactsActivity.class);
- addFlagsToIntent(intent);
- intent.putExtra("Contact", contact);
- startActivity(intent);
- }
-
- public void showContactsListForCreationOrEdition(Address address) {
- if (address == null) return;
-
- Intent intent = new Intent(this, ContactsActivity.class);
- addFlagsToIntent(intent);
- intent.putExtra("CreateOrEdit", true);
- intent.putExtra("SipUri", address.asStringUriOnly());
- if (address.getDisplayName() != null) {
- intent.putExtra("DisplayName", address.getDisplayName());
- }
- startActivity(intent);
- }
-
- public void showChatRoom(Address localAddress, Address peerAddress) {
- Intent intent = new Intent(this, ChatActivity.class);
- addFlagsToIntent(intent);
- if (localAddress != null) {
- intent.putExtra("LocalSipUri", localAddress.asStringUriOnly());
- }
- if (peerAddress != null) {
- intent.putExtra("RemoteSipUri", peerAddress.asStringUriOnly());
- }
- startActivity(intent);
- }
-
- // Dialogs
-
- public Dialog displayDialog(String text) {
- return LinphoneUtils.getDialog(this, text);
- }
-
- public void displayChatRoomError() {
- final Dialog dialog = displayDialog(getString(R.string.chat_room_creation_failed));
- dialog.findViewById(R.id.dialog_delete_button).setVisibility(View.GONE);
- Button cancel = dialog.findViewById(R.id.dialog_cancel_button);
- cancel.setText(getString(R.string.ok));
- cancel.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- dialog.dismiss();
- }
- });
-
- dialog.show();
- }
-
- private void displayDNDSettingsDialog() {
- if (!LinphonePreferences.instance().isDNDSettingsPopupEnabled()) return;
- Log.w("[Permission] Asking user to grant us permission to read DND settings");
-
- final Dialog dialog =
- displayDialog(getString(R.string.pref_grant_read_dnd_settings_permission_desc));
- dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE);
- final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain);
- dialog.findViewById(R.id.doNotAskAgainLabel)
- .setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- doNotAskAgain.setChecked(!doNotAskAgain.isChecked());
- }
- });
- Button cancel = dialog.findViewById(R.id.dialog_cancel_button);
- cancel.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (doNotAskAgain.isChecked()) {
- LinphonePreferences.instance().enableDNDSettingsPopup(false);
- }
- dialog.dismiss();
- }
- });
- Button ok = dialog.findViewById(R.id.dialog_ok_button);
- ok.setVisibility(View.VISIBLE);
- ok.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- try {
- startActivity(
- new Intent(
- "android.settings.NOTIFICATION_POLICY_ACCESS_SETTINGS"));
- } catch (ActivityNotFoundException anfe) {
- Log.e("[Main Activity] Activity not found exception: ", anfe);
- }
- dialog.dismiss();
- }
- });
- Button delete = dialog.findViewById(R.id.dialog_delete_button);
- delete.setVisibility(View.GONE);
- dialog.show();
- }
-
- public void isAccountWithAlias() {
- if (LinphoneManager.getCore().getDefaultProxyConfig() != null) {
- long now = new Timestamp(new Date().getTime()).getTime();
- AccountCreator accountCreator = LinphoneManager.getInstance().getAccountCreator();
- accountCreator.setListener(mAccountCreatorListener);
- if (LinphonePreferences.instance().getLinkPopupTime() == null
- || Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) < now) {
- accountCreator.reset();
- accountCreator.setUsername(
- LinphonePreferences.instance()
- .getAccountUsername(
- LinphonePreferences.instance().getDefaultAccountIndex()));
- accountCreator.isAccountExist();
- }
- } else {
- LinphonePreferences.instance().setLinkPopupTime(null);
- }
- }
-
- private void askLinkWithPhoneNumber() {
- if (!LinphonePreferences.instance().isLinkPopupEnabled()) return;
-
- long now = new Timestamp(new Date().getTime()).getTime();
- if (LinphonePreferences.instance().getLinkPopupTime() != null
- && Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) >= now) return;
-
- ProxyConfig proxyConfig = LinphoneManager.getCore().getDefaultProxyConfig();
- if (proxyConfig == null) return;
- if (!proxyConfig.getDomain().equals(getString(R.string.default_domain))) return;
-
- final Dialog dialog =
- LinphoneUtils.getDialog(
- this,
- String.format(
- getString(R.string.link_account_popup),
- proxyConfig.getIdentityAddress().asStringUriOnly()));
- Button delete = dialog.findViewById(R.id.dialog_delete_button);
- delete.setVisibility(View.GONE);
- Button ok = dialog.findViewById(R.id.dialog_ok_button);
- ok.setText(getString(R.string.link));
- ok.setVisibility(View.VISIBLE);
- Button cancel = dialog.findViewById(R.id.dialog_cancel_button);
- cancel.setText(getString(R.string.maybe_later));
-
- dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE);
- final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain);
- dialog.findViewById(R.id.doNotAskAgainLabel)
- .setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- doNotAskAgain.setChecked(!doNotAskAgain.isChecked());
- }
- });
-
- ok.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- Intent assistant = new Intent();
- assistant.setClass(
- MainActivity.this, PhoneAccountLinkingAssistantActivity.class);
- startActivity(assistant);
- updatePopupTimestamp();
- dialog.dismiss();
- }
- });
-
- cancel.setOnClickListener(
- new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- if (doNotAskAgain.isChecked()) {
- LinphonePreferences.instance().enableLinkPopup(false);
- }
- updatePopupTimestamp();
- dialog.dismiss();
- }
- });
- dialog.show();
- }
-
- private void updatePopupTimestamp() {
- long future =
- new Timestamp(
- getResources()
- .getInteger(
- R.integer.phone_number_linking_popup_time_interval))
- .getTime();
- long now = new Timestamp(new Date().getTime()).getTime();
- long newDate = now + future;
-
- LinphonePreferences.instance().setLinkPopupTime(String.valueOf(newDate));
- }
-
- // Logs
-
- private void shareUploadedLogsUrl(String info) {
- final String appName = getString(R.string.app_name);
-
- Intent i = new Intent(Intent.ACTION_SEND);
- i.putExtra(Intent.EXTRA_EMAIL, new String[] {getString(R.string.about_bugreport_email)});
- i.putExtra(Intent.EXTRA_SUBJECT, appName + " Logs");
- i.putExtra(Intent.EXTRA_TEXT, info);
- i.setType("application/zip");
-
- try {
- startActivity(Intent.createChooser(i, "Send mail..."));
- } catch (android.content.ActivityNotFoundException ex) {
- Log.e(ex);
- }
- }
-
- // Others
-
- public SideMenuFragment getSideMenuFragment() {
- return mSideMenuFragment;
- }
-}
diff --git a/app/src/main/java/org/linphone/call/views/CallIncomingButtonListener.java b/app/src/main/java/org/linphone/activities/SnackBarActivity.kt
similarity index 82%
rename from app/src/main/java/org/linphone/call/views/CallIncomingButtonListener.java
rename to app/src/main/java/org/linphone/activities/SnackBarActivity.kt
index 9127431ec..439999b58 100644
--- a/app/src/main/java/org/linphone/call/views/CallIncomingButtonListener.java
+++ b/app/src/main/java/org/linphone/activities/SnackBarActivity.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
@@ -17,8 +17,8 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.linphone.call.views;
+package org.linphone.activities
-public interface CallIncomingButtonListener {
- void onAction();
+interface SnackBarActivity {
+ fun showSnackBar(resourceId: Int)
}
diff --git a/app/src/main/java/org/linphone/activities/ThemeableActivity.java b/app/src/main/java/org/linphone/activities/ThemeableActivity.java
deleted file mode 100644
index ce5d10e6a..000000000
--- a/app/src/main/java/org/linphone/activities/ThemeableActivity.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for 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.pm.ActivityInfo;
-import android.content.res.Configuration;
-import android.os.Bundle;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.app.AppCompatDelegate;
-import org.linphone.R;
-import org.linphone.settings.LinphonePreferences;
-
-public abstract class ThemeableActivity extends AppCompatActivity {
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- if (getResources().getBoolean(R.bool.orientation_portrait_only)) {
- setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
- }
-
- super.onCreate(savedInstanceState);
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- int nightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
- switch (nightMode) {
- case Configuration.UI_MODE_NIGHT_NO:
- case Configuration.UI_MODE_NIGHT_UNDEFINED:
- if (LinphonePreferences.instance().isDarkModeEnabled()) {
- AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
- }
- case Configuration.UI_MODE_NIGHT_YES:
- if (!LinphonePreferences.instance().isDarkModeEnabled()) {
- AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
- }
- }
- }
-}
diff --git a/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt b/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt
new file mode 100644
index 000000000..b222e0b4c
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/AssistantActivity.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.databinding.DataBindingUtil
+import androidx.lifecycle.ViewModelProvider
+import com.google.android.material.snackbar.Snackbar
+import org.linphone.R
+import org.linphone.activities.GenericActivity
+import org.linphone.activities.SnackBarActivity
+import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
+import org.linphone.databinding.AssistantActivityBinding
+
+class AssistantActivity : GenericActivity(), SnackBarActivity {
+ private lateinit var binding: AssistantActivityBinding
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = DataBindingUtil.setContentView(this, R.layout.assistant_activity)
+
+ sharedViewModel = ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ }
+
+ override fun showSnackBar(resourceId: Int) {
+ Snackbar.make(binding.coordinator, resourceId, 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
new file mode 100644
index 000000000..0b36c1637
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/adapters/CountryPickerAdapter.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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
new file mode 100644
index 000000000..6e5416032
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/AbstractPhoneFragment.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.Manifest
+import android.app.AlertDialog
+import android.content.pm.PackageManager
+import androidx.fragment.app.Fragment
+import org.linphone.R
+import org.linphone.activities.assistant.viewmodels.AbstractPhoneViewModel
+import org.linphone.core.tools.Log
+import org.linphone.utils.PermissionHelper
+import org.linphone.utils.PhoneNumberUtils
+
+abstract class AbstractPhoneFragment : Fragment() {
+ abstract val viewModel: AbstractPhoneViewModel
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ if (requestCode == 0) {
+ if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ Log.i("[Assistant] READ_PHONE_NUMBERS permission granted")
+ updateFromDeviceInfo()
+ } else {
+ Log.w("[Assistant] READ_PHONE_NUMBERS permission denied")
+ }
+ }
+ }
+
+ protected fun checkPermission() {
+ if (!PermissionHelper.get().hasReadPhoneState()) {
+ Log.i("[Assistant] Asking for READ_PHONE_STATE permission")
+ requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), 0)
+ } else {
+ updateFromDeviceInfo()
+ }
+ }
+
+ private fun updateFromDeviceInfo() {
+ val phoneNumber = PhoneNumberUtils.getDevicePhoneNumber(requireContext())
+ val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(requireContext())
+ viewModel.updateFromPhoneNumberAndOrDialPlan(phoneNumber, dialPlan)
+ }
+
+ protected fun showPhoneNumberInfoDialog() {
+ AlertDialog.Builder(context)
+ .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
+ )
+ )
+ .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
new file mode 100644
index 000000000..30c5977e0
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/AccountLoginFragment.kt
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.assistant.viewmodels.AccountLoginViewModel
+import org.linphone.activities.assistant.viewmodels.AccountLoginViewModelFactory
+import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
+import org.linphone.databinding.AssistantAccountLoginFragmentBinding
+
+class AccountLoginFragment : AbstractPhoneFragment() {
+ private lateinit var binding: AssistantAccountLoginFragmentBinding
+ override lateinit var viewModel: AccountLoginViewModel
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantAccountLoginFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ viewModel = ViewModelProvider(this, AccountLoginViewModelFactory(sharedViewModel.getAccountCreator())).get(AccountLoginViewModel::class.java)
+ binding.viewModel = viewModel
+
+ binding.setInfoClickListener {
+ showPhoneNumberInfoDialog()
+ }
+
+ binding.setSelectCountryClickListener {
+ CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker")
+ }
+
+ viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ if (findNavController().currentDestination?.id == R.id.accountLoginFragment) {
+ val args = Bundle()
+ args.putBoolean("IsLogin", true)
+ args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
+ findNavController().navigate(R.id.action_accountLoginFragment_to_phoneAccountValidationFragment, args)
+ }
+ }
+ })
+
+ viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ if (coreContext.core.isEchoCancellerCalibrationRequired) {
+ if (findNavController().currentDestination?.id == R.id.accountLoginFragment) {
+ findNavController().navigate(R.id.action_accountLoginFragment_to_echoCancellerCalibrationFragment)
+ }
+ } else {
+ requireActivity().finish()
+ }
+ }
+ })
+
+ checkPermission()
+ }
+}
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
new file mode 100644
index 000000000..488a9cba5
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/CountryPickerFragment.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import org.linphone.activities.assistant.adapters.CountryPickerAdapter
+import org.linphone.core.DialPlan
+import org.linphone.databinding.AssistantCountryPickerFragmentBinding
+
+class CountryPickerFragment(private val listener: CountryPickedListener) : DialogFragment() {
+ private lateinit var binding: AssistantCountryPickerFragmentBinding
+ private lateinit var adapter: CountryPickerAdapter
+
+ 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) { }
+ })
+
+ 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
new file mode 100644
index 000000000..d1883fe34
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/EchoCancellerCalibrationFragment.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+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 : Fragment() {
+ private lateinit var binding: AssistantEchoCancellerCalibrationFragmentBinding
+ private lateinit var viewModel: EchoCancellerCalibrationViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantEchoCancellerCalibrationFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ viewModel = ViewModelProvider(this).get(EchoCancellerCalibrationViewModel::class.java)
+ binding.viewModel = viewModel
+
+ viewModel.echoCalibrationTerminated.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ requireActivity().finish()
+ }
+ })
+
+ if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
+ Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission")
+ requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 0)
+ } else {
+ viewModel.startEchoCancellerCalibration()
+ }
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ val granted = 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
new file mode 100644
index 000000000..ad1d394b4
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountCreationFragment.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.R
+import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModel
+import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModelFactory
+import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
+import org.linphone.databinding.AssistantEmailAccountCreationFragmentBinding
+
+class EmailAccountCreationFragment : Fragment() {
+ private lateinit var binding: AssistantEmailAccountCreationFragmentBinding
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+ private lateinit var viewModel: EmailAccountCreationViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantEmailAccountCreationFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ viewModel = ViewModelProvider(this, EmailAccountCreationViewModelFactory(sharedViewModel.getAccountCreator())).get(EmailAccountCreationViewModel::class.java)
+ binding.viewModel = viewModel
+
+ viewModel.goToEmailValidationEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ if (findNavController().currentDestination?.id == R.id.emailAccountCreationFragment) {
+ findNavController().navigate(R.id.action_emailAccountCreationFragment_to_emailAccountValidationFragment)
+ }
+ }
+ })
+ }
+}
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
new file mode 100644
index 000000000..3547ed61d
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/EmailAccountValidationFragment.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.R
+import org.linphone.activities.assistant.viewmodels.*
+import org.linphone.databinding.AssistantEmailAccountValidationFragmentBinding
+
+class EmailAccountValidationFragment : Fragment() {
+ private lateinit var binding: AssistantEmailAccountValidationFragmentBinding
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+ private lateinit var viewModel: EmailAccountValidationViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantEmailAccountValidationFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ viewModel = ViewModelProvider(this, EmailAccountValidationViewModelFactory(sharedViewModel.getAccountCreator())).get(EmailAccountValidationViewModel::class.java)
+ binding.viewModel = viewModel
+
+ viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ if (findNavController().currentDestination?.id == R.id.emailAccountValidationFragment) {
+ val args = Bundle()
+ args.putBoolean("AllowSkip", true)
+ args.putString("Username", viewModel.accountCreator.username)
+ args.putString("Password", viewModel.accountCreator.password)
+ findNavController().navigate(R.id.action_emailAccountValidationFragment_to_phoneAccountLinkingFragment, args)
+ }
+ }
+ })
+ }
+}
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
new file mode 100644
index 000000000..a05a290ac
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/GenericAccountLoginFragment.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.assistant.viewmodels.GenericLoginViewModel
+import org.linphone.activities.assistant.viewmodels.GenericLoginViewModelFactory
+import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
+import org.linphone.databinding.AssistantGenericAccountLoginFragmentBinding
+
+class GenericAccountLoginFragment : Fragment() {
+ private lateinit var binding: AssistantGenericAccountLoginFragmentBinding
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+ private lateinit var viewModel: GenericLoginViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantGenericAccountLoginFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ viewModel = ViewModelProvider(this, GenericLoginViewModelFactory(sharedViewModel.getAccountCreator(true))).get(GenericLoginViewModel::class.java)
+ binding.viewModel = viewModel
+
+ viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ if (coreContext.core.isEchoCancellerCalibrationRequired) {
+ if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) {
+ findNavController().navigate(R.id.action_genericAccountLoginFragment_to_echoCancellerCalibrationFragment)
+ }
+ } else {
+ requireActivity().finish()
+ }
+ }
+ })
+ }
+}
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
new file mode 100644
index 000000000..6b5c194d5
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountCreationFragment.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.R
+import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModel
+import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModelFactory
+import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
+import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
+
+class PhoneAccountCreationFragment : AbstractPhoneFragment() {
+ private lateinit var binding: AssistantPhoneAccountCreationFragmentBinding
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+ override lateinit var viewModel: PhoneAccountCreationViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantPhoneAccountCreationFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ viewModel = ViewModelProvider(this, PhoneAccountCreationViewModelFactory(sharedViewModel.getAccountCreator())).get(PhoneAccountCreationViewModel::class.java)
+ binding.viewModel = viewModel
+
+ binding.setInfoClickListener {
+ showPhoneNumberInfoDialog()
+ }
+
+ binding.setSelectCountryClickListener {
+ CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker")
+ }
+
+ viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ if (findNavController().currentDestination?.id == R.id.phoneAccountCreationFragment) {
+ val args = Bundle()
+ args.putBoolean("IsCreation", true)
+ args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
+ findNavController().navigate(R.id.action_phoneAccountCreationFragment_to_phoneAccountValidationFragment, args)
+ }
+ }
+ })
+
+ checkPermission()
+ }
+}
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
new file mode 100644
index 000000000..151e1fa72
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountLinkingFragment.kt
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.LinphoneApplication
+import org.linphone.R
+import org.linphone.activities.assistant.viewmodels.*
+import org.linphone.core.tools.Log
+import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
+
+class PhoneAccountLinkingFragment : AbstractPhoneFragment() {
+ private lateinit var binding: AssistantPhoneAccountLinkingFragmentBinding
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+ override lateinit var viewModel: PhoneAccountLinkingViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantPhoneAccountLinkingFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ val accountCreator = sharedViewModel.getAccountCreator()
+ viewModel = ViewModelProvider(this, PhoneAccountLinkingViewModelFactory(accountCreator)).get(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 {
+ CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker")
+ }
+
+ viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) {
+ val args = Bundle()
+ args.putBoolean("IsLinking", true)
+ args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
+ findNavController().navigate(R.id.action_phoneAccountLinkingFragment_to_phoneAccountValidationFragment, args)
+ }
+ }
+ })
+
+ viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) {
+ if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) {
+ if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) {
+ findNavController().navigate(R.id.action_phoneAccountLinkingFragment_to_echoCancellerCalibrationFragment)
+ }
+ } else {
+ requireActivity().finish()
+ }
+ }
+ }
+ })
+
+ checkPermission()
+ }
+}
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
new file mode 100644
index 000000000..930c2488e
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/PhoneAccountValidationFragment.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModel
+import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModelFactory
+import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
+import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding
+
+class PhoneAccountValidationFragment : Fragment() {
+ private lateinit var binding: AssistantPhoneAccountValidationFragmentBinding
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+ private lateinit var viewModel: PhoneAccountValidationViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantPhoneAccountValidationFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ viewModel = ViewModelProvider(this, PhoneAccountValidationViewModelFactory(sharedViewModel.getAccountCreator())).get(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, Observer {
+ it.consume {
+ when {
+ viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
+ if (coreContext.core.isEchoCancellerCalibrationRequired) {
+ if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) {
+ findNavController().navigate(R.id.action_phoneAccountValidationFragment_to_echoCancellerCalibrationFragment)
+ }
+ } else {
+ requireActivity().finish()
+ }
+ }
+ viewModel.isLinking.value == true -> {
+ if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) {
+ val args = Bundle()
+ args.putString("Identity", "sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}")
+ findNavController().navigate(R.id.action_phoneAccountValidationFragment_to_accountSettingsFragment, args)
+ }
+ }
+ }
+ }
+ })
+
+ 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) {
+ viewModel.code.value = clip
+ }
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..efb785649
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/QrCodeFragment.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.LinphoneApplication.Companion.coreContext
+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 : Fragment() {
+ private lateinit var binding: AssistantQrCodeFragmentBinding
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+ private lateinit var viewModel: QrCodeViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantQrCodeFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ viewModel = ViewModelProvider(this).get(QrCodeViewModel::class.java)
+ binding.viewModel = viewModel
+
+ viewModel.qrCodeFoundEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { url ->
+ sharedViewModel.remoteProvisioningUrl.value = url
+ findNavController().navigateUp()
+ }
+ })
+ viewModel.setBackCamera()
+
+ if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
+ Log.i("[QR Code] Asking for CAMERA permission")
+ requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 0)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture
+ coreContext.core.enableQrcodeVideoPreview(true)
+ coreContext.core.enableVideoPreview(true)
+ }
+
+ override fun onPause() {
+ coreContext.core.nativePreviewWindowId = null
+ coreContext.core.enableQrcodeVideoPreview(false)
+ coreContext.core.enableVideoPreview(false)
+
+ super.onPause()
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED
+ if (granted) {
+ Log.i("[QR Code] CAMERA permission granted")
+ } 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
new file mode 100644
index 000000000..2125479da
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/RemoteProvisioningFragment.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.util.Patterns
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.assistant.AssistantActivity
+import org.linphone.activities.assistant.viewmodels.RemoteProvisioningViewModel
+import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
+import org.linphone.databinding.AssistantRemoteProvisioningFragmentBinding
+
+class RemoteProvisioningFragment : Fragment() {
+ private lateinit var binding: AssistantRemoteProvisioningFragmentBinding
+ private lateinit var sharedViewModel: SharedAssistantViewModel
+ private lateinit var viewModel: RemoteProvisioningViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantRemoteProvisioningFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ viewModel = ViewModelProvider(this).get(RemoteProvisioningViewModel::class.java)
+ binding.viewModel = viewModel
+
+ binding.setApplyClickListener {
+ val url = viewModel.urlToFetch.value.orEmpty()
+ if (Patterns.WEB_URL.matcher(url).matches()) {
+ viewModel.fetchAndApply(url)
+ } else {
+ val activity = requireActivity() as AssistantActivity
+ activity.showSnackBar(R.string.assistant_remote_provisioning_wrong_format)
+ }
+ }
+
+ binding.setQrCodeClickListener {
+ if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) {
+ findNavController().navigate(R.id.action_remoteProvisioningFragment_to_qrCodeFragment)
+ }
+ }
+
+ viewModel.fetchSuccessfulEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { success ->
+ if (success) {
+ if (coreContext.core.isEchoCancellerCalibrationRequired) {
+ if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) {
+ findNavController().navigate(R.id.action_remoteProvisioningFragment_to_echoCancellerCalibrationFragment)
+ }
+ } else {
+ requireActivity().finish()
+ }
+ } else {
+ val activity = requireActivity() as AssistantActivity
+ activity.showSnackBar(R.string.assistant_remote_provisioning_failure)
+ }
+ }
+ })
+
+ viewModel.urlToFetch.value = sharedViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ sharedViewModel.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
new file mode 100644
index 000000000..85dd13c3a
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/TopBarFragment.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import org.linphone.databinding.AssistantTopBarFragmentBinding
+
+class TopBarFragment : Fragment() {
+ private lateinit var binding: AssistantTopBarFragmentBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantTopBarFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ binding.setBackClickListener {
+ if (!findNavController().popBackStack()) {
+ activity?.finish()
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..3802e7350
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/fragments/WelcomeFragment.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.navigation.fragment.findNavController
+import org.linphone.R
+import org.linphone.databinding.AssistantWelcomeFragmentBinding
+
+class WelcomeFragment : Fragment() {
+ private lateinit var binding: AssistantWelcomeFragmentBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AssistantWelcomeFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ binding.setCreateAccountClickListener {
+ if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
+ if (resources.getBoolean(R.bool.isTablet)) {
+ findNavController().navigate(R.id.action_welcomeFragment_to_emailAccountCreationFragment)
+ } else {
+ findNavController().navigate(R.id.action_welcomeFragment_to_phoneAccountCreationFragment)
+ }
+ }
+ }
+
+ binding.setAccountLoginClickListener {
+ if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
+ findNavController().navigate(R.id.action_welcomeFragment_to_accountLoginFragment)
+ }
+ }
+
+ binding.setGenericAccountLoginClickListener {
+ if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
+ findNavController().navigate(R.id.action_welcomeFragment_to_genericAccountLoginFragment)
+ }
+ }
+
+ binding.setRemoteProvisioningClickListener {
+ if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
+ findNavController().navigate(R.id.action_welcomeFragment_to_remoteProvisioningFragment)
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..c9a3b48de
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AbstractPhoneViewModel.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Transformations
+import androidx.lifecycle.ViewModel
+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(val accountCreator: AccountCreator) : ViewModel(),
+ CountryPickerFragment.CountryPickedListener {
+
+ val prefix = MutableLiveData()
+
+ val phoneNumber = MutableLiveData()
+ val phoneNumberError = MutableLiveData()
+
+ val countryName: LiveData = Transformations.switchMap(prefix) {
+ getCountryNameFromPrefix(it)
+ }
+
+ init {
+ prefix.value = "+"
+ }
+
+ override fun onCountryClicked(dialPlan: DialPlan) {
+ prefix.value = "+${dialPlan.countryCallingCode}"
+ }
+
+ fun isPhoneNumberOk(): Boolean {
+ return countryName.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && phoneNumberError.value.orEmpty().isEmpty()
+ }
+
+ fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
+ if (dialPlan != null) {
+ Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}")
+ prefix.value = "+${dialPlan.countryCallingCode}"
+ }
+ if (number != null) {
+ Log.i("[Assistant] Found phone number: $number")
+ phoneNumber.value = number
+ }
+ }
+
+ private fun getCountryNameFromPrefix(prefix: String?): MutableLiveData {
+ val country = MutableLiveData()
+ country.value = ""
+
+ if (prefix != null && prefix.isNotEmpty()) {
+ 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")
+ country.value = dialPlan?.country
+ }
+ return country
+ }
+}
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
new file mode 100644
index 000000000..4418e335e
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/AccountLoginViewModel.kt
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.ProxyConfig
+import org.linphone.core.tools.Log
+import org.linphone.utils.Event
+
+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 password = MutableLiveData()
+
+ val loginEnabled: MediatorLiveData = MediatorLiveData()
+
+ val waitForServerAnswer = MutableLiveData()
+
+ val leaveAssistantEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val goToSmsValidationEvent: 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 {
+ // TODO: show error
+ }
+ }
+ }
+
+ init {
+ accountCreator.addListener(listener)
+
+ loginWithUsernamePassword.value = false
+
+ 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()
+ }
+ }
+
+ override fun onCleared() {
+ accountCreator.removeListener(listener)
+ super.onCleared()
+ }
+
+ fun login() {
+ if (loginWithUsernamePassword.value == true) {
+ accountCreator.username = username.value
+ accountCreator.password = password.value
+ Log.i("[Assistant] [Account Login] Username is ${accountCreator.username}")
+
+ waitForServerAnswer.value = true
+ if (createProxyConfig()) {
+ leaveAssistantEvent.value = Event(true)
+ } else {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ } else {
+ accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
+ accountCreator.username = accountCreator.phoneNumber
+ Log.i("[Assistant] [Account Login] Phone number is ${accountCreator.phoneNumber}")
+
+ waitForServerAnswer.value = true
+ val status = accountCreator.recoverAccount()
+ Log.i("[Assistant] [Account Login] Recover account returned $status")
+ if (status != AccountCreator.Status.RequestOk) {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ }
+ }
+
+ private fun isLoginButtonEnabled(): Boolean {
+ return if (loginWithUsernamePassword.value == true) {
+ username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
+ } else {
+ isPhoneNumberOk()
+ }
+ }
+
+ private fun createProxyConfig(): Boolean {
+ val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
+
+ if (proxyConfig == null) {
+ Log.e("[Assistant] [Account Login] Account creator couldn't create proxy config")
+ // TODO: show error
+ return false
+ }
+
+ proxyConfig.isPushNotificationAllowed = true
+
+ Log.i("[Assistant] [Account Login] Proxy config 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
new file mode 100644
index 000000000..fe2c9ddce
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EchoCancellerCalibrationViewModel.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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("[Echo Canceller Calibration] Done, no echo")
+ }
+ EcCalibratorStatus.Done -> {
+ Log.i("[Echo Canceller Calibration] Done, delay is ${delay}ms")
+ }
+ EcCalibratorStatus.Failed -> {
+ Log.w("[Echo Canceller Calibration] Failed")
+ }
+ }
+ 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
new file mode 100644
index 000000000..b600419da
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountCreationViewModel.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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(val accountCreator: AccountCreator) : ViewModel() {
+ 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 createEnabled: MediatorLiveData = MediatorLiveData()
+
+ val waitForServerAnswer = MutableLiveData()
+
+ val goToEmailValidationEvent = 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
+ // TODO: show error
+ }
+ }
+ else -> {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ }
+ }
+
+ override fun onCreateAccount(
+ creator: AccountCreator,
+ status: AccountCreator.Status,
+ response: String?
+ ) {
+ Log.i("[Account Creation] onCreateAccount status is $status")
+ waitForServerAnswer.value = false
+
+ when (status) {
+ AccountCreator.Status.AccountCreated -> {
+ goToEmailValidationEvent.value = Event(true)
+ }
+ else -> {
+ // TODO: show error
+ }
+ }
+ }
+ }
+
+ 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()
+ }
+
+ fun create() {
+ accountCreator.username = username.value
+ accountCreator.password = password.value
+ accountCreator.email = email.value
+
+ waitForServerAnswer.value = true
+ val status = accountCreator.isAccountExist
+ Log.i("[Assistant] [Account Creation] Account exists returned $status")
+ if (status != AccountCreator.Status.RequestOk) {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ }
+
+ 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
new file mode 100644
index 000000000..e68b576a4
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/EmailAccountValidationViewModel.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.ProxyConfig
+import org.linphone.core.tools.Log
+import org.linphone.utils.Event
+
+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>()
+
+ private val listener = object : AccountCreatorListenerStub() {
+ override fun onIsAccountActivated(
+ creator: AccountCreator,
+ status: AccountCreator.Status,
+ response: String?
+ ) {
+ Log.i("[Account Validation] onIsAccountActivated status is $status")
+ waitForServerAnswer.value = false
+
+ when (status) {
+ AccountCreator.Status.AccountActivated -> {
+ if (createProxyConfig()) {
+ leaveAssistantEvent.value = Event(true)
+ } else {
+ // TODO: show error
+ }
+ }
+ AccountCreator.Status.AccountNotActivated -> {
+ // TODO: show error
+ }
+ else -> {
+ // TODO: show error
+ }
+ }
+ }
+ }
+
+ 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
+ // TODO: show error
+ }
+ }
+
+ private fun createProxyConfig(): Boolean {
+ val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
+
+ if (proxyConfig == null) {
+ Log.e("[Assistant] [Account Validation] Account creator couldn't create proxy config")
+ // TODO: show error
+ return false
+ }
+
+ proxyConfig.isPushNotificationAllowed = true
+
+ Log.i("[Assistant] [Account Validation] Proxy config 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
new file mode 100644
index 000000000..50f496704
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/GenericLoginViewModel.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.core.AccountCreator
+import org.linphone.core.ProxyConfig
+import org.linphone.core.TransportType
+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 leaveAssistantEvent = MutableLiveData>()
+
+ 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 createProxyConfig() {
+ accountCreator.username = username.value
+ accountCreator.password = password.value
+ accountCreator.domain = domain.value
+ accountCreator.displayName = displayName.value
+ accountCreator.transport = transport.value
+
+ val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
+
+ if (proxyConfig == null) {
+ Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config")
+ // TODO: show error
+ return
+ }
+
+ Log.i("[Assistant] [Generic Login] Proxy config created")
+ leaveAssistantEvent.value = Event(true)
+ }
+
+ 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
new file mode 100644
index 000000000..7e6ebceba
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountCreationViewModel.kt
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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 createEnabled: MediatorLiveData = MediatorLiveData()
+
+ val waitForServerAnswer = MutableLiveData()
+
+ val goToSmsValidationEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ private val listener = object : AccountCreatorListenerStub() {
+ override fun onIsAccountExist(
+ creator: AccountCreator,
+ status: AccountCreator.Status,
+ response: String?
+ ) {
+ Log.i("[Phone Account Creation] onIsAccountExist status is $status")
+ when (status) {
+ AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
+ 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.AccountNotExist -> {
+ val createAccountStatus = creator.createAccount()
+ Log.i("[Phone Account Creation] createAccount returned $createAccountStatus")
+ if (createAccountStatus != AccountCreator.Status.RequestOk) {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ }
+ else -> {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ }
+ }
+
+ override fun onCreateAccount(
+ creator: AccountCreator,
+ status: AccountCreator.Status,
+ response: String?
+ ) {
+ Log.i("[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 -> {
+ // TODO: show error
+ }
+ }
+ }
+ }
+
+ 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()
+ }
+ }
+
+ override fun onCleared() {
+ accountCreator.removeListener(listener)
+ super.onCleared()
+ }
+
+ fun create() {
+ accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
+ if (useUsername.value == true) {
+ accountCreator.username = username.value
+ } else {
+ accountCreator.username = accountCreator.phoneNumber
+ }
+
+ waitForServerAnswer.value = true
+ val status = accountCreator.isAccountExist
+ Log.i("[Phone Account Creation] isAccountExist returned $status")
+ if (status != AccountCreator.Status.RequestOk) {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ }
+
+ private fun isCreateButtonEnabled(): Boolean {
+ val usernameRegexp = corePreferences.config.getString("assistant", "username_regex", "^[a-z0-9+_.\\-]*\$")
+ return isPhoneNumberOk() &&
+ (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
new file mode 100644
index 000000000..788c61622
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountLinkingViewModel.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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>()
+
+ private val listener = object : AccountCreatorListenerStub() {
+ override fun onIsAliasUsed(
+ creator: AccountCreator,
+ status: AccountCreator.Status,
+ response: String?
+ ) {
+ Log.i("[Phone Account Linking] onIsAliasUsed status is $status")
+
+ when (status) {
+ AccountCreator.Status.AliasNotExist -> {
+ if (creator.linkAccount() != AccountCreator.Status.RequestOk) {
+ Log.e("[Phone Account Linking] linkAccount status is $status")
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ }
+ AccountCreator.Status.AliasExist, AccountCreator.Status.AliasIsAccount -> {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ else -> {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ }
+ }
+
+ override fun onLinkAccount(
+ creator: AccountCreator,
+ status: AccountCreator.Status,
+ response: String?
+ ) {
+ Log.i("[Phone Account Linking] onLinkAccount status is $status")
+ waitForServerAnswer.value = false
+
+ when (status) {
+ AccountCreator.Status.RequestOk -> {
+ goToSmsValidationEvent.value = Event(true)
+ }
+ else -> {
+ // TODO: show error
+ }
+ }
+ }
+ }
+
+ 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()
+ }
+ }
+
+ override fun onCleared() {
+ accountCreator.removeListener(listener)
+ super.onCleared()
+ }
+
+ fun link() {
+ accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
+ accountCreator.username = username.value
+ Log.i("[Assistant] [Phone Account Linking] Phone number is ${accountCreator.phoneNumber}")
+
+ waitForServerAnswer.value = true
+ val status: AccountCreator.Status = accountCreator.isAliasUsed
+ Log.i("[Phone Account Linking] isAliasUsed returned $status")
+ if (status != AccountCreator.Status.RequestOk) {
+ waitForServerAnswer.value = false
+ // TODO: show error
+ }
+ }
+
+ 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
new file mode 100644
index 000000000..ec2e7e2f0
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/PhoneAccountValidationViewModel.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.ProxyConfig
+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 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 (createProxyConfig()) {
+ leaveAssistantEvent.value = Event(true)
+ } else {
+ // TODO: show error
+ }
+ } else {
+ // TODO: show error
+ }
+ }
+
+ 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 -> {
+ // TODO: show error
+ }
+ }
+ }
+
+ 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 (createProxyConfig()) {
+ leaveAssistantEvent.value = Event(true)
+ } else {
+ // TODO: show error
+ }
+ } else {
+ // TODO: show error
+ }
+ }
+ }
+
+ 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
+ // TODO: show error
+ }
+ }
+
+ private fun createProxyConfig(): Boolean {
+ val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
+
+ if (proxyConfig == null) {
+ Log.e("[Assistant] [Phone Account Validation] Account creator couldn't create proxy config")
+ // TODO: show error
+ return false
+ }
+
+ proxyConfig.isPushNotificationAllowed = true
+ Log.i("[Assistant] [Phone Account Validation] Proxy config 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
new file mode 100644
index 000000000..ec3743957
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/QrCodeViewModel.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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("[QR Code] Found [$result]")
+ qrCodeFoundEvent.postValue(Event(result))
+ }
+ }
+
+ init {
+ coreContext.core.addListener(listener)
+ showSwitchCamera.value = coreContext.core.videoDevicesList.size > 1
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+ super.onCleared()
+ }
+
+ fun setBackCamera() {
+ for (camera in coreContext.core.videoDevicesList) {
+ if (camera.contains("Back")) {
+ Log.i("[QR Code] Found back facing camera: $camera")
+ coreContext.core.videoDevice = camera
+ return
+ }
+ }
+
+ val first = coreContext.core.videoDevicesList[0]
+ Log.i("[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
new file mode 100644
index 000000000..7da8e9b87
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/RemoteProvisioningViewModel.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.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 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)
+ }
+ }
+ }
+ }
+
+ init {
+ fetchInProgress.value = false
+ coreContext.core.addListener(listener)
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+ super.onCleared()
+ }
+
+ fun fetchAndApply(url: String) {
+ coreContext.core.provisioningUri = url
+ Log.w("[Remote Provisioning] Url set to [$url], restarting Core")
+ fetchInProgress.value = true
+ coreContext.core.stop()
+ coreContext.core.start()
+ }
+}
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
new file mode 100644
index 000000000..d97bdf1b7
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/assistant/viewmodels/SharedAssistantViewModel.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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/call/CallActivity.kt b/app/src/main/java/org/linphone/activities/call/CallActivity.kt
new file mode 100644
index 000000000..2ba58a1a5
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/CallActivity.kt
@@ -0,0 +1,205 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call
+
+import android.content.Context
+import android.content.res.Configuration
+import android.hardware.Sensor
+import android.hardware.SensorEvent
+import android.hardware.SensorEventListener
+import android.hardware.SensorManager
+import android.os.Bundle
+import android.os.PowerManager
+import android.view.Gravity
+import android.view.MotionEvent
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.GenericActivity
+import org.linphone.activities.call.viewmodels.ControlsFadingViewModel
+import org.linphone.activities.call.viewmodels.SharedCallViewModel
+import org.linphone.compatibility.Compatibility
+import org.linphone.core.tools.Log
+import org.linphone.databinding.CallActivityBinding
+
+class CallActivity : GenericActivity() {
+ private lateinit var binding: CallActivityBinding
+ private lateinit var viewModel: ControlsFadingViewModel
+ private lateinit var sharedViewModel: SharedCallViewModel
+
+ private var previewX: Float = 0f
+ private var previewY: Float = 0f
+ private lateinit var videoZoomHelper: VideoZoomHelper
+
+ private lateinit var sensorManager: SensorManager
+ private lateinit var proximitySensor: Sensor
+ private lateinit var proximityWakeLock: PowerManager.WakeLock
+ private val proximityListener: SensorEventListener = object : SensorEventListener {
+ override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { }
+
+ override fun onSensorChanged(event: SensorEvent) {
+ if (event.timestamp == 0L) return
+ if (isProximitySensorNearby(event)) {
+ if (!proximityWakeLock.isHeld) {
+ Log.i("[Call Activity] Acquiring proximity wake lock")
+ proximityWakeLock.acquire()
+ }
+ } else {
+ if (proximityWakeLock.isHeld) {
+ Log.i("[Call Activity] Releasing proximity wake lock")
+ proximityWakeLock.release()
+ }
+ }
+ }
+ }
+ private var proximitySensorEnabled = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
+ proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
+ proximityWakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
+ .newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "$packageName;proximity_sensor")
+
+ binding = DataBindingUtil.setContentView(this, R.layout.call_activity)
+ binding.lifecycleOwner = this
+
+ viewModel = ViewModelProvider(this).get(ControlsFadingViewModel::class.java)
+ binding.viewModel = viewModel
+
+ sharedViewModel = ViewModelProvider(this).get(SharedCallViewModel::class.java)
+
+ sharedViewModel.toggleDrawerEvent.observe(this, Observer {
+ it.consume {
+ if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) {
+ binding.statsMenu.closeDrawer(binding.sideMenuContent, true)
+ } else {
+ binding.statsMenu.openDrawer(binding.sideMenuContent, true)
+ }
+ }
+ })
+
+ coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
+ coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
+
+ binding.setPreviewTouchListener { v, event ->
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ previewX = v.x - event.rawX
+ previewY = v.y - event.rawY
+ }
+ MotionEvent.ACTION_MOVE -> {
+ v.animate().x(event.rawX + previewX).y(event.rawY + previewY).setDuration(0).start()
+ }
+ else -> false
+ }
+ true
+ }
+
+ videoZoomHelper = VideoZoomHelper(this, binding.remoteVideoSurface)
+
+ viewModel.videoEnabledEvent.observe(this, Observer {
+ it.consume { videoEnabled ->
+ enableProximitySensor(!videoEnabled)
+ }
+ })
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ if (coreContext.core.callsNb == 0) {
+ Log.w("[Call Activity] Resuming but no call found...")
+ finish()
+ } else {
+ coreContext.removeCallOverlay()
+
+ val currentCall = coreContext.core.currentCall ?: coreContext.core.calls[0]
+ if (currentCall != null) {
+ val videoEnabled = currentCall.currentParams.videoEnabled()
+ enableProximitySensor(!videoEnabled)
+ }
+ }
+ }
+
+ override fun onPause() {
+ enableProximitySensor(false)
+
+ val core = coreContext.core
+ if (core.callsNb > 0) {
+ coreContext.createCallOverlay()
+ }
+
+ super.onPause()
+ }
+
+ override fun onUserLeaveHint() {
+ super.onUserLeaveHint()
+
+ if (coreContext.core.currentCall?.currentParams?.videoEnabled() == true) {
+ Compatibility.enterPipMode(this)
+ }
+ }
+
+ override fun onPictureInPictureModeChanged(
+ isInPictureInPictureMode: Boolean,
+ newConfig: Configuration
+ ) {
+ if (isInPictureInPictureMode) {
+ viewModel.areControlsHidden.value = true
+ }
+ }
+
+ private fun enableProximitySensor(enable: Boolean) {
+ if (enable) {
+ if (!proximitySensorEnabled) {
+ Log.i("[Call Activity] Enabling proximity sensor listener")
+ sensorManager.registerListener(proximityListener, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL)
+ proximitySensorEnabled = true
+ }
+ } else {
+ if (proximitySensorEnabled) {
+ Log.i("[Call Activity] Disabling proximity sensor listener")
+ sensorManager.unregisterListener(proximityListener)
+ if (proximityWakeLock.isHeld) {
+ proximityWakeLock.release()
+ }
+ proximitySensorEnabled = false
+ }
+ }
+ }
+
+ private fun isProximitySensorNearby(event: SensorEvent): Boolean {
+ var threshold = 4.001f // <= 4 cm is near
+
+ val distanceInCm = event.values[0]
+ val maxDistance = event.sensor.maximumRange
+ Log.d("[Call Activity] Proximity sensor report [$distanceInCm] , for max range [$maxDistance]")
+
+ if (maxDistance <= threshold) {
+ // Case binary 0/1 and short sensors
+ threshold = maxDistance
+ }
+ return distanceInCm < threshold
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt
new file mode 100644
index 000000000..a112409c4
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/IncomingCallActivity.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call
+
+import android.annotation.TargetApi
+import android.app.KeyguardManager
+import android.content.Context
+import android.os.Bundle
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.GenericActivity
+import org.linphone.activities.call.viewmodels.IncomingCallViewModel
+import org.linphone.activities.call.viewmodels.IncomingCallViewModelFactory
+import org.linphone.core.Call
+import org.linphone.core.tools.Log
+import org.linphone.databinding.CallIncomingActivityBinding
+import org.linphone.mediastream.Version
+import org.linphone.utils.PermissionHelper
+
+class IncomingCallActivity : GenericActivity() {
+ private lateinit var binding: CallIncomingActivityBinding
+ private lateinit var viewModel: IncomingCallViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = DataBindingUtil.setContentView(this, R.layout.call_incoming_activity)
+ binding.lifecycleOwner = this
+
+ var incomingCall: Call? = null
+ for (call in coreContext.core.calls) {
+ if (call.state == Call.State.IncomingReceived ||
+ call.state == Call.State.IncomingEarlyMedia) {
+ incomingCall = call
+ }
+ }
+
+ if (incomingCall == null) {
+ Log.e("[Incoming Call] Couldn't find call in state Incoming")
+ finish()
+ return
+ }
+
+ viewModel = ViewModelProvider(
+ this,
+ IncomingCallViewModelFactory(incomingCall)
+ )[IncomingCallViewModel::class.java]
+ binding.viewModel = viewModel
+
+ viewModel.callEndedEvent.observe(this, Observer {
+ it.consume {
+ finish()
+ }
+ })
+
+ val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
+ viewModel.screenLocked.value = keyguardManager.isKeyguardLocked
+
+ binding.buttons.setViewModel(viewModel)
+
+ if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
+ checkPermissions()
+ }
+ }
+
+ @TargetApi(Version.API23_MARSHMALLOW_60)
+ private fun checkPermissions() {
+ val permissionsRequiredList = arrayListOf()
+ if (!PermissionHelper.get().hasRecordAudioPermission()) {
+ Log.i("[Incoming Call] Asking for RECORD_AUDIO permission")
+ permissionsRequiredList.add(android.Manifest.permission.RECORD_AUDIO)
+ }
+ if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) {
+ Log.i("[Incoming Call] Asking for CAMERA permission")
+ permissionsRequiredList.add(android.Manifest.permission.CAMERA)
+ }
+ if (permissionsRequiredList.isNotEmpty()) {
+ val permissionsRequired = arrayOfNulls(permissionsRequiredList.size)
+ permissionsRequiredList.toArray(permissionsRequired)
+ requestPermissions(permissionsRequired, 0)
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt b/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt
new file mode 100644
index 000000000..3ef548c14
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/OutgoingCallActivity.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call
+
+import android.annotation.TargetApi
+import android.os.Bundle
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.GenericActivity
+import org.linphone.activities.call.viewmodels.CallViewModel
+import org.linphone.activities.call.viewmodels.CallViewModelFactory
+import org.linphone.activities.call.viewmodels.ControlsViewModel
+import org.linphone.core.Call
+import org.linphone.core.tools.Log
+import org.linphone.databinding.CallOutgoingActivityBinding
+import org.linphone.mediastream.Version
+import org.linphone.utils.PermissionHelper
+
+class OutgoingCallActivity : GenericActivity() {
+ private lateinit var binding: CallOutgoingActivityBinding
+ private lateinit var viewModel: CallViewModel
+ private lateinit var controlsViewModel: ControlsViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = DataBindingUtil.setContentView(this, R.layout.call_outgoing_activity)
+ binding.lifecycleOwner = this
+
+ var outgoingCall: Call? = null
+ for (call in coreContext.core.calls) {
+ if (call.state == Call.State.OutgoingInit ||
+ call.state == Call.State.OutgoingProgress ||
+ call.state == Call.State.OutgoingRinging) {
+ outgoingCall = call
+ }
+ }
+
+ if (outgoingCall == null) {
+ Log.e("[Outgoing Call] Couldn't find call in state Outgoing")
+ finish()
+ return
+ }
+
+ viewModel = ViewModelProvider(
+ this,
+ CallViewModelFactory(outgoingCall)
+ )[CallViewModel::class.java]
+ binding.viewModel = viewModel
+
+ controlsViewModel = ViewModelProvider(this).get(ControlsViewModel::class.java)
+ binding.controlsViewModel = controlsViewModel
+
+ binding.setTerminateCallClickListener {
+ viewModel.terminateCall()
+ }
+
+ binding.setToggleMicrophoneClickListener {
+ if (PermissionHelper.get().hasRecordAudioPermission()) {
+ controlsViewModel.toggleMuteMicrophone()
+ } else {
+ checkPermissions()
+ }
+ }
+
+ binding.setToggleSpeakerClickListener {
+ controlsViewModel.toggleSpeaker()
+ }
+
+ viewModel.callEndedEvent.observe(this, Observer {
+ it.consume {
+ finish()
+ }
+ })
+
+ if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
+ checkPermissions()
+ }
+ }
+
+ @TargetApi(Version.API23_MARSHMALLOW_60)
+ private fun checkPermissions() {
+ val permissionsRequiredList = arrayListOf()
+ if (!PermissionHelper.get().hasRecordAudioPermission()) {
+ Log.i("[Outgoing Call] Asking for RECORD_AUDIO permission")
+ permissionsRequiredList.add(android.Manifest.permission.RECORD_AUDIO)
+ }
+ if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) {
+ Log.i("[Outgoing Call] Asking for CAMERA permission")
+ permissionsRequiredList.add(android.Manifest.permission.CAMERA)
+ }
+ if (permissionsRequiredList.isNotEmpty()) {
+ val permissionsRequired = arrayOfNulls(permissionsRequiredList.size)
+ permissionsRequiredList.toArray(permissionsRequired)
+ requestPermissions(permissionsRequired, 0)
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt b/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt
new file mode 100644
index 000000000..392d0d4fe
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/VideoZoomHelper.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call
+
+import android.content.Context
+import android.view.GestureDetector
+import android.view.MotionEvent
+import android.view.ScaleGestureDetector
+import android.view.View
+import kotlin.math.max
+import kotlin.math.min
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.Call
+
+class VideoZoomHelper(context: Context, private var videoDisplayView: View) : GestureDetector.SimpleOnGestureListener() {
+ private var scaleDetector: ScaleGestureDetector
+
+ private var zoomFactor = 1f
+ private var zoomCenterX = 0f
+ private var zoomCenterY = 0f
+
+ init {
+ val gestureDetector = GestureDetector(context, this)
+
+ scaleDetector = ScaleGestureDetector(context, object :
+ ScaleGestureDetector.SimpleOnScaleGestureListener() {
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
+ zoomFactor *= detector.scaleFactor
+ // Don't let the object get too small or too large.
+ // Zoom to make the video fill the screen vertically
+ val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
+ // Zoom to make the video fill the screen horizontally
+ val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
+ zoomFactor = max(0.1f, min(zoomFactor, max(portraitZoomFactor, landscapeZoomFactor)))
+
+ val currentCall: Call? = coreContext.core.currentCall
+ if (currentCall != null) {
+ currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
+ return true
+ }
+
+ return false
+ }
+ })
+
+ videoDisplayView.setOnTouchListener { v, event ->
+ val currentZoomFactor = zoomFactor
+ scaleDetector.onTouchEvent(event)
+
+ if (currentZoomFactor != zoomFactor) {
+ // We did scale, prevent touch event from going further
+ return@setOnTouchListener true
+ }
+
+ // If true, gesture detected, prevent touch event from going further
+ // Otherwise it seems we didn't use event,
+ // allow it to be dispatched somewhere else
+ gestureDetector.onTouchEvent(event)
+ }
+ }
+
+ override fun onScroll(
+ e1: MotionEvent,
+ e2: MotionEvent,
+ distanceX: Float,
+ distanceY: Float
+ ): Boolean {
+ val currentCall: Call? = coreContext.core.currentCall
+ if (currentCall != null) {
+ if (zoomFactor > 1) {
+ // Video is zoomed, slide is used to change center of zoom
+ if (distanceX > 0 && zoomCenterX < 1) {
+ zoomCenterX += 0.01f
+ } else if (distanceX < 0 && zoomCenterX > 0) {
+ zoomCenterX -= 0.01f
+ }
+
+ if (distanceY < 0 && zoomCenterY < 1) {
+ zoomCenterY += 0.01f
+ } else if (distanceY > 0 && zoomCenterY > 0) {
+ zoomCenterY -= 0.01f
+ }
+
+ if (zoomCenterX > 1) zoomCenterX = 1f
+ if (zoomCenterX < 0) zoomCenterX = 0f
+ if (zoomCenterY > 1) zoomCenterY = 1f
+ if (zoomCenterY < 0) zoomCenterY = 0f
+
+ currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
+ return true
+ }
+ }
+
+ return false
+ }
+
+ override fun onDoubleTap(e: MotionEvent?): Boolean {
+ val currentCall: Call? = coreContext.core.currentCall
+ if (currentCall != null) {
+ if (zoomFactor == 1f) {
+ // Zoom to make the video fill the screen vertically
+ val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
+ // Zoom to make the video fill the screen horizontally
+ val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
+ zoomFactor = max(portraitZoomFactor, landscapeZoomFactor)
+ } else {
+ resetZoom()
+ }
+
+ currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
+ return true
+ }
+
+ return false
+ }
+
+ private fun resetZoom() {
+ zoomFactor = 1f
+ zoomCenterY = 0.5f
+ zoomCenterX = zoomCenterY
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt
new file mode 100644
index 000000000..02c0193ea
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/fragments/ControlsFragment.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.fragments
+
+import android.content.Intent
+import android.os.Bundle
+import android.os.SystemClock
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import org.linphone.activities.call.viewmodels.CallsViewModel
+import org.linphone.activities.call.viewmodels.ControlsViewModel
+import org.linphone.activities.main.MainActivity
+import org.linphone.databinding.CallControlsFragmentBinding
+
+class ControlsFragment : Fragment() {
+ private lateinit var binding: CallControlsFragmentBinding
+ private lateinit var callsViewModel: CallsViewModel
+ private lateinit var controlsViewModel: ControlsViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = CallControlsFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ callsViewModel = ViewModelProvider(this).get(CallsViewModel::class.java)
+ binding.viewModel = callsViewModel
+
+ controlsViewModel = ViewModelProvider(this).get(ControlsViewModel::class.java)
+ binding.controlsViewModel = controlsViewModel
+
+ callsViewModel.currentCallViewModel.observe(viewLifecycleOwner, Observer {
+ if (it != null) {
+ binding.activeCallTimer.base =
+ SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
+ binding.activeCallTimer.start()
+ }
+ })
+
+ callsViewModel.noMoreCallEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ activity?.finish()
+ }
+ })
+
+ controlsViewModel.chatClickedEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ val intent = Intent()
+ intent.setClass(requireContext(), MainActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+ intent.putExtra("Chat", true)
+ startActivity(intent)
+ }
+ })
+
+ controlsViewModel.addCallClickedEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ val intent = Intent()
+ intent.setClass(requireContext(), MainActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+ intent.putExtra("Dialer", true)
+ intent.putExtra("Transfer", false)
+ startActivity(intent)
+ }
+ })
+
+ controlsViewModel.transferCallClickedEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ val intent = Intent()
+ intent.setClass(requireContext(), MainActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+ intent.putExtra("Dialer", true)
+ intent.putExtra("Transfer", true)
+ startActivity(intent)
+ }
+ })
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/fragments/StatisticsFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/StatisticsFragment.kt
new file mode 100644
index 000000000..7e9bc8d57
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/fragments/StatisticsFragment.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.fragments
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import org.linphone.activities.call.viewmodels.StatisticsListViewModel
+import org.linphone.databinding.CallStatisticsFragmentBinding
+
+class StatisticsFragment : Fragment() {
+ private lateinit var binding: CallStatisticsFragmentBinding
+ private lateinit var viewModel: StatisticsListViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = CallStatisticsFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ viewModel = ViewModelProvider(this).get(StatisticsListViewModel::class.java)
+ binding.viewModel = viewModel
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt b/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt
new file mode 100644
index 000000000..9e819759d
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/fragments/StatusFragment.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.fragments
+
+import android.app.Dialog
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import java.util.*
+import org.linphone.R
+import org.linphone.activities.call.viewmodels.SharedCallViewModel
+import org.linphone.activities.call.viewmodels.StatusViewModel
+import org.linphone.activities.main.viewmodels.DialogViewModel
+import org.linphone.core.Call
+import org.linphone.core.tools.Log
+import org.linphone.databinding.CallStatusFragmentBinding
+import org.linphone.utils.DialogUtils
+import org.linphone.utils.Event
+
+class StatusFragment : Fragment() {
+ private lateinit var binding: CallStatusFragmentBinding
+ private lateinit var viewModel: StatusViewModel
+ private lateinit var sharedViewModel: SharedCallViewModel
+ private var zrtpDialog: Dialog? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = CallStatusFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ viewModel = ViewModelProvider(this).get(StatusViewModel::class.java)
+ binding.viewModel = viewModel
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedCallViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ binding.setStatsClickListener {
+ sharedViewModel.toggleDrawerEvent.value = Event(true)
+ }
+
+ binding.setRefreshClickListener {
+ viewModel.refreshRegister()
+ }
+
+ viewModel.showZrtpDialogEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { call ->
+ if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) {
+ showZrtpDialog(call)
+ }
+ }
+ })
+ }
+
+ override fun onDestroy() {
+ if (zrtpDialog != null) {
+ zrtpDialog?.dismiss()
+ }
+ super.onDestroy()
+ }
+
+ private fun showZrtpDialog(call: Call) {
+ if (zrtpDialog != null && zrtpDialog?.isShowing == true) {
+ Log.e("[Status Fragment] ZRTP dialog already visible")
+ return
+ }
+
+ 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_message), getString(R.string.zrtp_dialog_title))
+ viewModel.showZrtp = true
+ viewModel.zrtpReadSas = toRead.toUpperCase(Locale.getDefault())
+ viewModel.zrtpListenSas = toListen.toUpperCase(Locale.getDefault())
+ viewModel.showIcon = true
+ viewModel.iconResource = R.drawable.security_2_indicator
+
+ val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
+
+ viewModel.showDeleteButton({
+ call.authenticationTokenVerified = false
+ this@StatusFragment.viewModel.updateEncryptionInfo(call)
+ dialog.dismiss()
+ zrtpDialog = null
+ }, getString(R.string.zrtp_dialog_deny_button_label))
+
+ viewModel.showOkButton({
+ call.authenticationTokenVerified = true
+ this@StatusFragment.viewModel.updateEncryptionInfo(call)
+ dialog.dismiss()
+ zrtpDialog = null
+ }, getString(R.string.zrtp_dialog_ok_button_label))
+
+ zrtpDialog = dialog
+ dialog.show()
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallStatisticsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallStatisticsViewModel.kt
new file mode 100644
index 000000000..ab38d079d
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/CallStatisticsViewModel.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.contact.GenericContactViewModel
+import org.linphone.core.*
+
+class CallStatisticsViewModel(val call: Call) : GenericContactViewModel(call.remoteAddress) {
+ val audioStats = MutableLiveData>()
+
+ val videoStats = MutableLiveData>()
+
+ val isVideoEnabled = MutableLiveData()
+
+ val isExpanded = MutableLiveData()
+
+ private val listener = object : CoreListenerStub() {
+ override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
+ if (call == this@CallStatisticsViewModel.call) {
+ isVideoEnabled.value = call.currentParams.videoEnabled()
+ updateCallStats(stats)
+ }
+ }
+ }
+
+ init {
+ coreContext.core.addListener(listener)
+
+ audioStats.value = arrayListOf()
+ videoStats.value = arrayListOf()
+
+ initCallStats()
+
+ val videoEnabled = call.currentParams.videoEnabled()
+ isVideoEnabled.value = videoEnabled
+
+ isExpanded.value = coreContext.core.currentCall == call
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+
+ super.onCleared()
+ }
+
+ fun toggleExpanded() {
+ isExpanded.value = isExpanded.value != true
+ }
+
+ private fun initCallStats() {
+ val audioList = arrayListOf()
+ audioList.add(StatItemViewModel(StatType.CAPTURE))
+ audioList.add(StatItemViewModel(StatType.PLAYBACK))
+ audioList.add(StatItemViewModel(StatType.PAYLOAD))
+ audioList.add(StatItemViewModel(StatType.ENCODER))
+ audioList.add(StatItemViewModel(StatType.DECODER))
+ audioList.add(StatItemViewModel(StatType.DOWNLOAD_BW))
+ audioList.add(StatItemViewModel(StatType.UPLOAD_BW))
+ audioList.add(StatItemViewModel(StatType.ICE))
+ audioList.add(StatItemViewModel(StatType.IP_FAM))
+ audioList.add(StatItemViewModel(StatType.SENDER_LOSS))
+ audioList.add(StatItemViewModel(StatType.RECEIVER_LOSS))
+ audioList.add(StatItemViewModel(StatType.JITTER))
+ audioStats.value = audioList
+
+ val videoList = arrayListOf()
+ videoList.add(StatItemViewModel(StatType.CAPTURE))
+ videoList.add(StatItemViewModel(StatType.PLAYBACK))
+ videoList.add(StatItemViewModel(StatType.PAYLOAD))
+ videoList.add(StatItemViewModel(StatType.ENCODER))
+ videoList.add(StatItemViewModel(StatType.DECODER))
+ videoList.add(StatItemViewModel(StatType.DOWNLOAD_BW))
+ videoList.add(StatItemViewModel(StatType.UPLOAD_BW))
+ videoList.add(StatItemViewModel(StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW))
+ videoList.add(StatItemViewModel(StatType.ICE))
+ videoList.add(StatItemViewModel(StatType.IP_FAM))
+ videoList.add(StatItemViewModel(StatType.SENDER_LOSS))
+ videoList.add(StatItemViewModel(StatType.RECEIVER_LOSS))
+ videoList.add(StatItemViewModel(StatType.SENT_RESOLUTION))
+ videoList.add(StatItemViewModel(StatType.RECEIVED_RESOLUTION))
+ videoList.add(StatItemViewModel(StatType.SENT_FPS))
+ videoList.add(StatItemViewModel(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)
+ }
+ } 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/call/viewmodels/CallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt
new file mode 100644
index 000000000..3330a6144
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/CallViewModel.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.contact.GenericContactViewModel
+import org.linphone.core.Call
+import org.linphone.core.CallListenerStub
+import org.linphone.core.tools.Log
+import org.linphone.utils.Event
+
+class CallViewModelFactory(private val call: Call) :
+ ViewModelProvider.NewInstanceFactory() {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return CallViewModel(call) as T
+ }
+}
+
+open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAddress) {
+ val address: String by lazy {
+ call.remoteAddress.clean() // To remove gruu if any
+ call.remoteAddress.asStringUriOnly()
+ }
+
+ val isPaused = MutableLiveData()
+
+ val callEndedEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ private val listener = object : CallListenerStub() {
+ override fun onStateChanged(call: Call, state: Call.State, message: String) {
+ if (call != this@CallViewModel.call) return
+
+ isPaused.value = state == Call.State.Paused
+
+ if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
+ callEndedEvent.value = Event(true)
+
+ if (state == Call.State.Error) {
+ Log.e("[Call View Model] Error state reason is ${call.reason}")
+ }
+ }
+ }
+ }
+
+ init {
+ call.addListener(listener)
+
+ isPaused.value = call.state == Call.State.Paused
+ }
+
+ override fun onCleared() {
+ call.removeListener(listener)
+
+ super.onCleared()
+ }
+
+ fun terminateCall() {
+ coreContext.terminateCall(call)
+ }
+
+ fun pause() {
+ call.pause()
+ }
+
+ fun resume() {
+ call.resume()
+ }
+
+ fun removeFromConference() {
+ if (call.conference != null) {
+ call.conference.removeParticipant(call.remoteAddress)
+ if (call.core.conferenceSize <= 1) call.core.leaveConference()
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt
new file mode 100644
index 000000000..d20364988
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/CallsViewModel.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.Call
+import org.linphone.core.Core
+import org.linphone.core.CoreListenerStub
+import org.linphone.utils.Event
+
+class CallsViewModel : ViewModel() {
+ val currentCallViewModel = MutableLiveData()
+
+ val callPausedByRemote = MutableLiveData()
+
+ val pausedCalls = MutableLiveData>()
+
+ val conferenceCalls = MutableLiveData>()
+
+ val isConferencePaused = MutableLiveData()
+
+ val noMoreCallEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ private val listener = object : CoreListenerStub() {
+ override fun onCallStateChanged(
+ core: Core,
+ call: Call,
+ state: Call.State,
+ message: String
+ ) {
+ callPausedByRemote.value = state == Call.State.PausedByRemote
+ isConferencePaused.value = !coreContext.core.isInConference
+
+ if (core.currentCall == null) {
+ currentCallViewModel.value = null
+ } else if (currentCallViewModel.value == null) {
+ currentCallViewModel.value = CallViewModel(core.currentCall)
+ }
+
+ if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
+ if (core.callsNb == 0) {
+ noMoreCallEvent.value = Event(true)
+ conferenceCalls.value = arrayListOf()
+ } else {
+ removeCallFromPausedListIfPresent(call)
+ removeCallFromConferenceIfPresent(call)
+ }
+ } else {
+ if (state == Call.State.Pausing) {
+ addCallToPausedList(call)
+ } else if (state == Call.State.Resuming) {
+ removeCallFromPausedListIfPresent(call)
+ } else {
+ if (call.conference != null) {
+ addCallToConferenceListIfNotAlreadyInIt(call)
+ } else {
+ removeCallFromConferenceIfPresent(call)
+ }
+ }
+ }
+ }
+ }
+
+ init {
+ coreContext.core.addListener(listener)
+
+ val currentCall = coreContext.core.currentCall
+ if (currentCall != null) {
+ currentCallViewModel.value = CallViewModel(currentCall)
+ }
+ callPausedByRemote.value = currentCall?.state == Call.State.PausedByRemote
+ isConferencePaused.value = !coreContext.core.isInConference
+
+ val conferenceList = arrayListOf()
+ for (call in coreContext.core.calls) {
+ if (call.state == Call.State.Paused || call.state == Call.State.Pausing) {
+ addCallToPausedList(call)
+ } else {
+ if (call.conference != null && call.core.isInConference) {
+ conferenceList.add(CallViewModel(call))
+ }
+ }
+ }
+ conferenceCalls.value = conferenceList
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+
+ super.onCleared()
+ }
+
+ fun pauseConference() {
+ if (coreContext.core.isInConference) {
+ coreContext.core.leaveConference()
+ isConferencePaused.value = true
+ }
+ }
+
+ fun resumeConference() {
+ if (!coreContext.core.isInConference) {
+ coreContext.core.enterConference()
+ isConferencePaused.value = false
+ }
+ }
+
+ private fun addCallToPausedList(call: Call) {
+ val list = arrayListOf()
+ list.addAll(pausedCalls.value.orEmpty())
+
+ val viewModel = CallViewModel(call)
+ list.add(viewModel)
+ pausedCalls.value = list
+ }
+
+ private fun removeCallFromPausedListIfPresent(call: Call) {
+ val list = arrayListOf()
+ list.addAll(pausedCalls.value.orEmpty())
+
+ for (pausedCallViewModel in list) {
+ if (pausedCallViewModel.call == call) {
+ list.remove(pausedCallViewModel)
+ break
+ }
+ }
+
+ pausedCalls.value = list
+ }
+
+ private fun addCallToConferenceListIfNotAlreadyInIt(call: Call) {
+ val list = arrayListOf()
+ list.addAll(conferenceCalls.value.orEmpty())
+
+ for (viewModel in list) {
+ if (viewModel.call == call) return
+ }
+
+ val viewModel = CallViewModel(call)
+ list.add(viewModel)
+ conferenceCalls.value = list
+ }
+
+ private fun removeCallFromConferenceIfPresent(call: Call) {
+ val list = arrayListOf()
+ list.addAll(conferenceCalls.value.orEmpty())
+
+ for (viewModel in list) {
+ if (viewModel.call == call) {
+ list.remove(viewModel)
+ break
+ }
+ }
+
+ conferenceCalls.value = list
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt
new file mode 100644
index 000000000..2a64907ac
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsFadingViewModel.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import java.util.*
+import org.linphone.LinphoneApplication.Companion.coreContext
+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.Event
+
+class ControlsFadingViewModel : ViewModel() {
+ val areControlsHidden = MutableLiveData()
+
+ val videoEnabledEvent = MutableLiveData>()
+
+ private var timer: Timer? = null
+
+ private val listener = object : CoreListenerStub() {
+ override fun onCallStateChanged(
+ core: Core,
+ call: Call,
+ state: Call.State,
+ message: String?
+ ) {
+ if (state == Call.State.StreamsRunning || state == Call.State.Updating || state == Call.State.UpdatedByRemote) {
+ Log.i("[Controls Fading] Call is in state $state, video is enabled? ${call.currentParams.videoEnabled()}")
+ if (call.currentParams.videoEnabled()) {
+ videoEnabledEvent.value = Event(true)
+ startTimer()
+ } else {
+ videoEnabledEvent.value = Event(false)
+ stopTimer()
+ }
+ }
+ }
+ }
+
+ init {
+ coreContext.core.addListener(listener)
+
+ areControlsHidden.value = false
+
+ val currentCall = coreContext.core.currentCall
+ if (currentCall != null && currentCall.currentParams.videoEnabled()) {
+ videoEnabledEvent.value = Event(true)
+ startTimer()
+ }
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+ stopTimer()
+
+ super.onCleared()
+ }
+
+ fun showMomentarily() {
+ stopTimer()
+ startTimer()
+ }
+
+ private fun stopTimer() {
+ timer?.cancel()
+
+ areControlsHidden.value = false
+ }
+
+ private fun startTimer() {
+ timer?.cancel()
+
+ timer = Timer("Hide UI controls scheduler")
+ timer?.schedule(object : TimerTask() {
+ override fun run() {
+ areControlsHidden.postValue(coreContext.core.currentCall?.currentParams?.videoEnabled() ?: false)
+ }
+ }, 3000)
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt
new file mode 100644
index 000000000..dc9c86dab
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/ControlsViewModel.kt
@@ -0,0 +1,317 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import kotlin.math.max
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.activities.main.dialer.NumpadDigitListener
+import org.linphone.core.*
+import org.linphone.core.tools.Log
+import org.linphone.utils.Event
+import org.linphone.utils.PermissionHelper
+
+class ControlsViewModel : ViewModel() {
+ val isMicrophoneMuted = MutableLiveData()
+
+ val isMuteMicrophoneEnabled = MutableLiveData()
+
+ val isSpeakerSelected = MutableLiveData()
+
+ val isBluetoothHeadsetSelected = MutableLiveData()
+
+ val isVideoAvailable = MutableLiveData()
+
+ val isVideoEnabled = MutableLiveData()
+
+ val isVideoUpdateInProgress = MutableLiveData()
+
+ val isPauseEnabled = MutableLiveData()
+
+ val isRecording = MutableLiveData()
+
+ val isConferencingAvailable = MutableLiveData()
+
+ val unreadMessagesCount = MutableLiveData()
+
+ val numpadVisibility = MutableLiveData()
+
+ val optionsVisibility = MutableLiveData()
+
+ val audioRoutesVisibility = MutableLiveData()
+
+ val audioRoutesEnabled = MutableLiveData()
+
+ val chatClickedEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val addCallClickedEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val transferCallClickedEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val onKeyClick: NumpadDigitListener = object : NumpadDigitListener {
+ override fun handleClick(key: Char) {
+ coreContext.core.playDtmf(key, 1)
+ }
+
+ override fun handleLongClick(key: Char): Boolean {
+ return true
+ }
+ }
+
+ private val listener: CoreListenerStub = object : CoreListenerStub() {
+ override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
+ updateUnreadChatCount()
+ }
+
+ override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
+ updateUnreadChatCount()
+ }
+
+ override fun onCallStateChanged(
+ core: Core,
+ call: Call,
+ state: Call.State,
+ message: String?
+ ) {
+ if (state == Call.State.StreamsRunning) isVideoUpdateInProgress.value = false
+
+ updateUI()
+ }
+
+ override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) {
+ updateAudioRelated()
+ }
+
+ override fun onAudioDevicesListUpdated(core: Core) {
+ updateAudioRelated()
+ }
+ }
+
+ init {
+ coreContext.core.addListener(listener)
+ val currentCall = coreContext.core.currentCall
+
+ updateMuteMicState()
+ updateAudioRelated()
+ updateUnreadChatCount()
+
+ numpadVisibility.value = false
+ optionsVisibility.value = false
+ audioRoutesVisibility.value = false
+
+ isRecording.value = currentCall?.isRecording
+ isVideoUpdateInProgress.value = false
+
+ updateUI()
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+ super.onCleared()
+ }
+
+ fun updateUnreadChatCount() {
+ unreadMessagesCount.value = coreContext.core.unreadChatMessageCountFromActiveLocals
+ }
+
+ fun toggleMuteMicrophone() {
+ val micEnabled = coreContext.core.micEnabled()
+ coreContext.core.enableMic(!micEnabled)
+ updateMuteMicState()
+ }
+
+ fun toggleSpeaker() {
+ val audioDevice = coreContext.core.outputAudioDevice
+ if (audioDevice?.type == AudioDevice.Type.Speaker) {
+ forceEarpieceAudioRoute()
+ } else {
+ forceSpeakerAudioRoute()
+ }
+ }
+
+ fun switchCamera() {
+ coreContext.switchCamera()
+ }
+
+ fun terminateCall() {
+ val core = coreContext.core
+ when {
+ core.currentCall != null -> core.currentCall.terminate()
+ core.isInConference -> core.terminateConference()
+ else -> core.terminateAllCalls()
+ }
+ }
+
+ fun toggleVideo() {
+ 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)
+ return
+
+ isVideoUpdateInProgress.value = true
+ val params = core.createCallParams(currentCall)
+ params.enableVideo(!currentCall.currentParams.videoEnabled())
+ currentCall.update(params)
+ }
+ }
+
+ fun toggleOptionsMenu() {
+ optionsVisibility.value = optionsVisibility.value != true
+ }
+
+ fun toggleNumpadVisibility() {
+ numpadVisibility.value = numpadVisibility.value != true
+ }
+
+ fun toggleRoutesMenu() {
+ audioRoutesVisibility.value = audioRoutesVisibility.value != true
+ }
+
+ fun toggleRecording(closeMenu: Boolean) {
+ val currentCall = coreContext.core.currentCall
+ if (currentCall != null) {
+ if (currentCall.isRecording) {
+ currentCall.stopRecording()
+ } else {
+ currentCall.startRecording()
+ }
+ }
+ isRecording.value = currentCall?.isRecording
+ if (closeMenu) toggleOptionsMenu()
+ }
+
+ fun onChatClicked() {
+ chatClickedEvent.value = Event(true)
+ }
+
+ fun onAddCallClicked() {
+ addCallClickedEvent.value = Event(true)
+ toggleOptionsMenu()
+ }
+
+ fun onTransferCallClicked() {
+ transferCallClickedEvent.value = Event(true)
+ toggleOptionsMenu()
+ }
+
+ fun startConference() {
+ coreContext.core.addAllToConference()
+ toggleOptionsMenu()
+ }
+
+ fun forceEarpieceAudioRoute() {
+ for (audioDevice in coreContext.core.audioDevices) {
+ if (audioDevice.type == AudioDevice.Type.Earpiece) {
+ Log.i("[Call] Found earpiece audio device [${audioDevice.deviceName}], routing audio to it")
+ coreContext.core.outputAudioDevice = audioDevice
+ return
+ }
+ }
+ Log.e("[Call] Couldn't find earpiece audio device")
+ }
+
+ fun forceSpeakerAudioRoute() {
+ for (audioDevice in coreContext.core.audioDevices) {
+ if (audioDevice.type == AudioDevice.Type.Speaker) {
+ Log.i("[Call] Found speaker audio device [${audioDevice.deviceName}], routing audio to it")
+ coreContext.core.outputAudioDevice = audioDevice
+ return
+ }
+ }
+ Log.e("[Call] Couldn't find speaker audio device")
+ }
+
+ fun forceBluetoothAudioRoute() {
+ for (audioDevice in coreContext.core.audioDevices) {
+ if (audioDevice.type == AudioDevice.Type.Bluetooth) {
+ Log.i("[Call] Found bluetooth audio device [${audioDevice.deviceName}], routing audio to it")
+ coreContext.core.outputAudioDevice = audioDevice
+ return
+ }
+ }
+ Log.e("[Call] Couldn't find bluetooth audio device")
+ }
+
+ private fun updateAudioRelated() {
+ updateSpeakerState()
+ updateBluetoothHeadsetState()
+ updateAudioRoutesState()
+ }
+
+ private fun updateUI() {
+ val currentCall = coreContext.core.currentCall
+ updateVideoAvailable()
+ updateVideoEnabled()
+ isPauseEnabled.value = currentCall != null && !currentCall.mediaInProgress()
+ isMuteMicrophoneEnabled.value = currentCall != null || coreContext.core.isInConference
+ updateConferenceState()
+ }
+
+ private fun updateMuteMicState() {
+ isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.micEnabled()
+ }
+
+ private fun updateSpeakerState() {
+ val audioDevice = coreContext.core.outputAudioDevice
+ isSpeakerSelected.value = audioDevice?.type == AudioDevice.Type.Speaker
+ }
+
+ private fun updateAudioRoutesState() {
+ var bluetoothDeviceAvailable = false
+ for (audioDevice in coreContext.core.audioDevices) {
+ if (audioDevice.type == AudioDevice.Type.Bluetooth) {
+ bluetoothDeviceAvailable = true
+ break
+ }
+ }
+ audioRoutesEnabled.value = bluetoothDeviceAvailable
+ }
+
+ private fun updateBluetoothHeadsetState() {
+ val audioDevice = coreContext.core.outputAudioDevice
+ isBluetoothHeadsetSelected.value = audioDevice?.type == AudioDevice.Type.Bluetooth
+ }
+
+ private fun updateVideoAvailable() {
+ val core = coreContext.core
+ isVideoAvailable.value = (core.videoCaptureEnabled() || core.videoPreviewEnabled()) &&
+ core.currentCall != null && !core.currentCall.mediaInProgress()
+ }
+
+ private fun updateVideoEnabled() {
+ val core = coreContext.core
+ isVideoEnabled.value = core.currentCall?.currentParams?.videoEnabled()
+ }
+
+ private fun updateConferenceState() {
+ val core = coreContext.core
+ isConferencingAvailable.value = core.callsNb > max(1, core.conferenceSize) && !core.soundResourcesLocked()
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt
new file mode 100644
index 000000000..1aff1a300
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/IncomingCallViewModel.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.*
+
+class IncomingCallViewModelFactory(private val call: Call) :
+ ViewModelProvider.NewInstanceFactory() {
+
+ @Suppress("UNCHECKED_CAST")
+ override fun create(modelClass: Class): T {
+ return IncomingCallViewModel(call) as T
+ }
+}
+
+class IncomingCallViewModel(call: Call) : CallViewModel(call) {
+ val screenLocked = MutableLiveData()
+
+ val earlyMediaVideoEnabled = MutableLiveData()
+
+ val inviteWithVideo = MutableLiveData()
+
+ init {
+ screenLocked.value = false
+ inviteWithVideo.value = call.currentParams.videoEnabled()
+ earlyMediaVideoEnabled.value = call.state == Call.State.IncomingEarlyMedia && call.currentParams?.videoEnabled() ?: false
+ }
+
+ fun answer(doAction: Boolean) {
+ if (doAction) coreContext.answerCall(call)
+ }
+
+ fun decline(doAction: Boolean) {
+ if (doAction) coreContext.declineCall(call)
+ }
+}
diff --git a/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureListener.java b/app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt
similarity index 69%
rename from app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureListener.java
rename to app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt
index 53e035ef6..791c4bdda 100644
--- a/app/src/main/java/org/linphone/compatibility/CompatibilityScaleGestureListener.java
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/SharedCallViewModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
@@ -17,8 +17,12 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.linphone.compatibility;
+package org.linphone.activities.call.viewmodels
-public interface CompatibilityScaleGestureListener {
- boolean onScale(CompatibilityScaleGestureDetector detector);
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.linphone.utils.Event
+
+class SharedCallViewModel : ViewModel() {
+ val toggleDrawerEvent = MutableLiveData>()
}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/SingleStatisticViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/SingleStatisticViewModel.kt
new file mode 100644
index 000000000..674343a10
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/SingleStatisticViewModel.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import java.text.DecimalFormat
+import org.linphone.R
+import org.linphone.core.AddressFamily
+import org.linphone.core.Call
+import org.linphone.core.CallStats
+import org.linphone.core.StreamType
+
+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)
+}
+
+class StatItemViewModel(val type: StatType) : ViewModel() {
+ val value = MutableLiveData()
+
+ fun update(call: Call, stats: CallStats) {
+ val payloadType = if (stats.type == StreamType.Audio) call.currentParams.usedAudioPayloadType else call.currentParams.usedVideoPayloadType
+ value.value = when (type) {
+ StatType.CAPTURE -> if (stats.type == StreamType.Audio) call.core.captureDevice else call.core.videoDevice
+ StatType.PLAYBACK -> if (stats.type == StreamType.Audio) call.core.playbackDevice 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 == AddressFamily.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"
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt
new file mode 100644
index 000000000..2f49f30e0
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/StatisticsListViewModel.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.viewmodels
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.Call
+import org.linphone.core.Core
+import org.linphone.core.CoreListenerStub
+
+class StatisticsListViewModel : ViewModel() {
+ val callStatsList = MutableLiveData>()
+
+ 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) {
+ for (stat in callStatsList.value.orEmpty()) {
+ if (stat.call == call) {
+ callStatsList.value?.remove(stat)
+ }
+ }
+ }
+ }
+ }
+
+ init {
+ coreContext.core.addListener(listener)
+
+ 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) {
+ list.add(CallStatisticsViewModel(call))
+ }
+ }
+ callStatsList.value = list
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+
+ super.onCleared()
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt b/app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt
new file mode 100644
index 000000000..4943e865f
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/viewmodels/StatusViewModel.kt
@@ -0,0 +1,153 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.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>()
+ }
+
+ 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
+ ) {
+ if (call.params.mediaEncryption == MediaEncryption.ZRTP && !call.authenticationTokenVerified) {
+ showZrtpDialogEvent.value = Event(call)
+ } else {
+ updateEncryptionInfo(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)
+
+ if (currentCall.params.mediaEncryption == MediaEncryption.ZRTP && !currentCall.authenticationTokenVerified) {
+ showZrtpDialogEvent.value = Event(currentCall)
+ }
+ }
+ }
+
+ override fun onCleared() {
+ coreContext.core.removeListener(listener)
+
+ super.onCleared()
+ }
+
+ fun showZrtpDialog() {
+ val currentCall = coreContext.core.currentCall
+ if (currentCall?.params?.mediaEncryption == MediaEncryption.ZRTP) {
+ showZrtpDialogEvent.value = Event(currentCall)
+ }
+ }
+
+ 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
+ }
+
+ when (call.params.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) {
+ true -> R.drawable.security_ok
+ else -> R.drawable.security_pending
+ }
+ encryptionContentDescription.value = when (call.authenticationTokenVerified) {
+ 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 updateCallQualityIcon() {
+ val call = coreContext.core.currentCall
+ 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/call/views/AnswerDeclineIncomingCallButtons.kt b/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt
new file mode 100644
index 000000000..fb20b1184
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/views/AnswerDeclineIncomingCallButtons.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.views
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.MotionEvent
+import android.view.View
+import android.view.View.OnTouchListener
+import android.widget.LinearLayout
+import androidx.databinding.DataBindingUtil
+import org.linphone.R
+import org.linphone.activities.call.viewmodels.IncomingCallViewModel
+import org.linphone.core.tools.Log
+import org.linphone.databinding.CallIncomingAnswerDeclineButtonsBinding
+
+class AnswerDeclineIncomingCallButtons : LinearLayout {
+ private lateinit var binding: CallIncomingAnswerDeclineButtonsBinding
+ private var mBegin = false
+ private var mDeclineX = 0f
+ private var mAnswerX = 0f
+ private var mOldSize = 0f
+
+ private val mAnswerTouchListener = OnTouchListener { view, motionEvent ->
+ val curX: Float
+
+ when (motionEvent.action) {
+ MotionEvent.ACTION_DOWN -> {
+ binding.declineButton.visibility = View.GONE
+ mAnswerX = motionEvent.x - view.width
+ mBegin = true
+ mOldSize = 0f
+ }
+ MotionEvent.ACTION_MOVE -> {
+ curX = motionEvent.x - view.width
+ view.scrollBy((mAnswerX - curX).toInt(), view.scrollY)
+ mOldSize -= mAnswerX - curX
+ mAnswerX = curX
+ if (mOldSize < -25) mBegin = false
+ if (curX < (width / 4) - view.width && !mBegin) {
+ binding.viewModel?.answer(true)
+ }
+ }
+ MotionEvent.ACTION_UP -> {
+ binding.declineButton.visibility = View.VISIBLE
+ view.scrollTo(0, view.scrollY)
+ }
+ }
+ true
+ }
+ private val mDeclineTouchListener = OnTouchListener { view, motionEvent ->
+ val curX: Float
+
+ when (motionEvent.action) {
+ MotionEvent.ACTION_DOWN -> {
+ binding.answerButton.visibility = View.GONE
+ mDeclineX = motionEvent.x
+ }
+ MotionEvent.ACTION_MOVE -> {
+ curX = motionEvent.x
+ view.scrollBy((mDeclineX - curX).toInt(), view.scrollY)
+ mDeclineX = curX
+ if (curX > 3 * width / 4) {
+ binding.viewModel?.decline(true)
+ }
+ }
+ MotionEvent.ACTION_UP -> {
+ binding.answerButton.visibility = View.VISIBLE
+ view.scrollTo(0, view.scrollY)
+ }
+ }
+ true
+ }
+
+ constructor(context: Context) : super(context) {
+ init(context)
+ }
+
+ constructor(context: Context, attrs: AttributeSet) : super(
+ context,
+ attrs
+ ) {
+ init(context)
+ }
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr) {
+ init(context)
+ }
+
+ fun setViewModel(viewModel: IncomingCallViewModel) {
+ binding.viewModel = viewModel
+
+ updateSlideMode()
+ }
+
+ private fun init(context: Context) {
+ binding = DataBindingUtil.inflate(
+ LayoutInflater.from(context), R.layout.call_incoming_answer_decline_buttons, this, true
+ )
+
+ updateSlideMode()
+ }
+
+ private fun updateSlideMode() {
+ val slideMode = binding.viewModel?.screenLocked?.value == true
+ Log.i("[Call Incoming Decline Button] Slide mode is $slideMode")
+ if (slideMode) {
+ binding.answerButton.setOnTouchListener(mAnswerTouchListener)
+ binding.declineButton.setOnTouchListener(mDeclineTouchListener)
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/views/ConferenceCallView.kt b/app/src/main/java/org/linphone/activities/call/views/ConferenceCallView.kt
new file mode 100644
index 000000000..35d709ffa
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/views/ConferenceCallView.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.views
+
+import android.content.Context
+import android.os.SystemClock
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import androidx.databinding.DataBindingUtil
+import org.linphone.R
+import org.linphone.activities.call.viewmodels.CallViewModel
+import org.linphone.databinding.CallConferenceBinding
+
+class ConferenceCallView : LinearLayout {
+ private lateinit var binding: CallConferenceBinding
+
+ constructor(context: Context) : super(context) {
+ init(context)
+ }
+
+ constructor(context: Context, attrs: AttributeSet) : super(
+ context,
+ attrs
+ ) {
+ init(context)
+ }
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr) {
+ init(context)
+ }
+
+ fun init(context: Context) {
+ binding = DataBindingUtil.inflate(
+ LayoutInflater.from(context), R.layout.call_conference, this, true
+ )
+ }
+
+ fun setViewModel(viewModel: CallViewModel) {
+ binding.viewModel = viewModel
+
+ binding.callTimer.base =
+ SystemClock.elapsedRealtime() - (1000 * viewModel.call.duration) // Linphone timestamps are in seconds
+ binding.callTimer.start()
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt b/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt
new file mode 100644
index 000000000..a5c9ffce4
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/call/views/PausedCallView.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.call.views
+
+import android.content.Context
+import android.os.SystemClock
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.LinearLayout
+import androidx.databinding.DataBindingUtil
+import org.linphone.R
+import org.linphone.activities.call.viewmodels.CallViewModel
+import org.linphone.databinding.CallPausedBinding
+
+class PausedCallView : LinearLayout {
+ private lateinit var binding: CallPausedBinding
+
+ constructor(context: Context) : super(context) {
+ init(context)
+ }
+
+ constructor(context: Context, attrs: AttributeSet) : super(
+ context,
+ attrs
+ ) {
+ init(context)
+ }
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr) {
+ init(context)
+ }
+
+ fun init(context: Context) {
+ binding = DataBindingUtil.inflate(
+ LayoutInflater.from(context), R.layout.call_paused, this, true
+ )
+ }
+
+ fun setViewModel(viewModel: CallViewModel) {
+ binding.viewModel = viewModel
+
+ binding.callTimer.base =
+ SystemClock.elapsedRealtime() - (1000 * viewModel.call.duration) // Linphone timestamps are in seconds
+ binding.callTimer.start()
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt b/app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt
new file mode 100644
index 000000000..0e9783ba3
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/launcher/LauncherActivity.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.launcher
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.appcompat.app.AppCompatActivity
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.main.MainActivity
+import org.linphone.core.tools.Log
+
+class LauncherActivity : AppCompatActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(R.layout.launcher_activity)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ coreContext.handler.postDelayed({ onReady() }, 500)
+ }
+
+ private fun onReady() {
+ Log.i("[Launcher] Core is ready")
+ val intent = Intent()
+ intent.setClass(this, MainActivity::class.java)
+
+ // Propagate current intent action, type and data
+ if (getIntent() != null) {
+ val extras = getIntent().extras
+ if (extras != null) intent.putExtras(extras)
+ }
+ intent.action = getIntent().action
+ intent.type = getIntent().type
+ intent.data = getIntent().data
+
+ startActivity(intent)
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/MainActivity.kt b/app/src/main/java/org/linphone/activities/main/MainActivity.kt
new file mode 100644
index 000000000..9b71daa48
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/MainActivity.kt
@@ -0,0 +1,215 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.os.Parcelable
+import android.view.Gravity
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.findNavController
+import com.google.android.material.snackbar.Snackbar
+import java.io.UnsupportedEncodingException
+import java.net.URLDecoder
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.LinphoneApplication.Companion.corePreferences
+import org.linphone.R
+import org.linphone.activities.GenericActivity
+import org.linphone.activities.SnackBarActivity
+import org.linphone.activities.assistant.AssistantActivity
+import org.linphone.activities.call.CallActivity
+import org.linphone.activities.main.viewmodels.SharedMainViewModel
+import org.linphone.compatibility.Compatibility
+import org.linphone.contact.ContactsUpdatedListenerStub
+import org.linphone.core.tools.Log
+import org.linphone.databinding.MainActivityBinding
+import org.linphone.utils.AppUtils
+import org.linphone.utils.FileUtils
+
+class MainActivity : GenericActivity(), SnackBarActivity {
+ private lateinit var binding: MainActivityBinding
+ private lateinit var sharedViewModel: SharedMainViewModel
+
+ private val listener = object : ContactsUpdatedListenerStub() {
+ override fun onContactsUpdated() {
+ if (corePreferences.contactsShortcuts) {
+ Log.i("[Main Activity] Contact(s) updated, update shortcuts")
+ Compatibility.createShortcutsToContacts(this@MainActivity)
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
+ binding.lifecycleOwner = this
+
+ sharedViewModel = ViewModelProvider(this).get(SharedMainViewModel::class.java)
+ binding.viewModel = sharedViewModel
+
+ sharedViewModel.toggleDrawerEvent.observe(this, Observer {
+ it.consume {
+ if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
+ binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
+ } else {
+ binding.sideMenu.openDrawer(binding.sideMenuContent, true)
+ }
+ }
+ })
+
+ binding.setGoBackToCallClickListener {
+ val intent = Intent(this, CallActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+ startActivity(intent)
+ }
+
+ if (intent != null) handleIntentParams(intent)
+
+ if (coreContext.core.proxyConfigList.isEmpty()) {
+ if (corePreferences.firstStart) {
+ corePreferences.firstStart = false
+ startActivity(Intent(this, AssistantActivity::class.java))
+ }
+ }
+ }
+
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+
+ if (intent != null) handleIntentParams(intent)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ coreContext.contactsManager.addListener(listener)
+ }
+
+ override fun onPause() {
+ coreContext.contactsManager.removeListener(listener)
+ super.onPause()
+ }
+
+ override fun showSnackBar(resourceId: Int) {
+ Snackbar.make(binding.coordinator, resourceId, Snackbar.LENGTH_LONG).show()
+ }
+
+ private fun handleIntentParams(intent: Intent) {
+ when (intent.action) {
+ Intent.ACTION_SEND -> {
+ handleSendImage(intent)
+ }
+ Intent.ACTION_SEND_MULTIPLE -> {
+ handleSendMultipleImages(intent)
+ }
+ Intent.ACTION_VIEW -> {
+ if (intent.type == AppUtils.getString(R.string.linphone_address_mime_type)) {
+ val contactUri = intent.data
+ if (contactUri != null) {
+ val contactId = coreContext.contactsManager.getAndroidContactIdFromUri(contactUri)
+ if (contactId != null) {
+ val deepLink = "linphone-android://contact/view/$contactId"
+ Log.i("[Main Activity] Found contact URI parameter in intent: $contactUri, starting deep link: $deepLink")
+ findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
+ }
+ }
+ }
+ }
+ Intent.ACTION_DIAL, Intent.ACTION_CALL -> {
+ val uri = intent.data
+ if (uri != null) {
+ Log.i("[Main Activity] Found uri: $uri to call")
+ val stringUri = uri.toString()
+ var addressToCall: String = stringUri
+ try {
+ addressToCall = URLDecoder.decode(stringUri, "UTF-8")
+ } catch (e: UnsupportedEncodingException) {}
+
+ if (addressToCall.startsWith("sip:")) {
+ addressToCall = addressToCall.substring("sip:".length)
+ } else if (addressToCall.startsWith("tel:")) {
+ addressToCall = addressToCall.substring("tel:".length)
+ }
+
+ Log.i("[Main Activity] Starting dialer with pre-filled URI $addressToCall")
+ val args = Bundle()
+ args.putString("URI", addressToCall)
+ findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_dialerFragment, args)
+ }
+ }
+ else -> {
+ when {
+ intent.hasExtra("ContactId") -> {
+ val id = intent.getStringExtra("ContactId")
+ val deepLink = "linphone-android://contact/view/$id"
+ Log.i("[Main Activity] Found contact id parameter in intent: $id, starting deep link: $deepLink")
+ findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
+ }
+ intent.hasExtra("Chat") -> {
+ Log.i("[Main Activity] Found chat intent extra, go to chat rooms list")
+ findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment)
+ }
+ intent.hasExtra("Dialer") -> {
+ Log.i("[Main Activity] Found dialer intent extra, go to dialer")
+ val args = Bundle()
+ args.putBoolean("Transfer", intent.getBooleanExtra("Transfer", false))
+ findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_dialerFragment, args)
+ }
+ }
+ }
+ }
+ }
+
+ private fun handleSendImage(intent: Intent) {
+ (intent.getParcelableExtra(Intent.EXTRA_STREAM) as? Uri)?.let {
+ val list = arrayListOf()
+ val path = FileUtils.getFilePath(this, it)
+ if (path != null) {
+ list.add(path)
+ Log.i("[Main Activity] Found single file to share: $path")
+ }
+ sharedViewModel.filesToShare.value = list
+
+ val deepLink = "linphone-android://chat/"
+ Log.i("[Main Activity] Starting deep link: $deepLink")
+ findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
+ }
+ }
+
+ private fun handleSendMultipleImages(intent: Intent) {
+ intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.let {
+ val list = arrayListOf()
+ for (parcelable in it) {
+ val uri = parcelable as Uri
+ val path = FileUtils.getFilePath(this, uri)
+ Log.i("[Main Activity] Found file to share: $path")
+ if (path != null) list.add(path)
+ }
+ sharedViewModel.filesToShare.value = list
+
+ val deepLink = "linphone-android://chat/"
+ Log.i("[Main Activity] Starting deep link: $deepLink")
+ findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
+ }
+ }
+}
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
new file mode 100644
index 000000000..db7e329f1
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/about/AboutFragment.kt
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.R
+import org.linphone.activities.main.MainActivity
+import org.linphone.core.tools.Log
+import org.linphone.databinding.AboutFragmentBinding
+
+class AboutFragment : Fragment() {
+ private lateinit var binding: AboutFragmentBinding
+ private lateinit var viewModel: AboutViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = AboutFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ viewModel = ViewModelProvider(this).get(AboutViewModel::class.java)
+ binding.viewModel = viewModel
+
+ binding.setBackClickListener { findNavController().popBackStack() }
+
+ binding.setPrivacyPolicyClickListener {
+ val browserIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(getString(R.string.about_privacy_policy_link))
+ )
+ startActivity(browserIntent)
+ }
+
+ binding.setLicenseClickListener {
+ val browserIntent = Intent(
+ Intent.ACTION_VIEW,
+ Uri.parse(getString(R.string.about_license_link))
+ )
+ startActivity(browserIntent)
+ }
+
+ viewModel.uploadFinishedEvent.observe(viewLifecycleOwner, Observer {
+ 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)
+
+ shareUploadedLogsUrl(url)
+ }
+ })
+ }
+
+ // Logs
+ private fun shareUploadedLogsUrl(info: String) {
+ val appName = getString(R.string.app_name)
+ val intent = Intent(Intent.ACTION_SEND)
+ intent.putExtra(
+ Intent.EXTRA_EMAIL,
+ arrayOf(getString(R.string.about_bugreport_email))
+ )
+ intent.putExtra(Intent.EXTRA_SUBJECT, "$appName Logs")
+ intent.putExtra(Intent.EXTRA_TEXT, info)
+ intent.type = "application/zip"
+
+ try {
+ startActivity(Intent.createChooser(intent, "Send mail..."))
+ } catch (ex: ActivityNotFoundException) {
+ Log.e(ex)
+ }
+ }
+}
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
new file mode 100644
index 000000000..5857bddb9
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/about/AboutViewModel.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.MutableLiveData
+import androidx.lifecycle.ViewModel
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.LinphoneApplication.Companion.corePreferences
+import org.linphone.core.Core
+import org.linphone.core.CoreListenerStub
+import org.linphone.utils.Event
+
+class AboutViewModel : ViewModel() {
+ val appVersion: String = coreContext.appVersion
+
+ val sdkVersion: String = coreContext.sdkVersion
+
+ val showLogsButtons: Boolean = corePreferences.debugLogs
+
+ val uploadInProgress = MutableLiveData()
+
+ val uploadFinishedEvent: 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
+ }
+ }
+ }
+
+ 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()
+ }
+}
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
new file mode 100644
index 000000000..7b2598b83
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/ChatScrollListener.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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
+
+ // 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
+ }
+
+ // 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)
+
+ 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
new file mode 100644
index 000000000..9f8129cab
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/GroupChatRoomMember.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.ChatRoomSecurityLevel
+
+data class GroupChatRoomMember(
+ val address: Address,
+ var isAdmin: Boolean = false,
+ val securityLevel: ChatRoomSecurityLevel = ChatRoomSecurityLevel.ClearText,
+ val hasLimeX3DHCapability: 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
new file mode 100644
index 000000000..74bdb9d89
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
@@ -0,0 +1,330 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.MenuItem
+import android.view.ViewGroup
+import androidx.appcompat.widget.PopupMenu
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DiffUtil
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.main.chat.viewmodels.ChatMessageViewModel
+import org.linphone.activities.main.chat.viewmodels.EventViewModel
+import org.linphone.activities.main.chat.viewmodels.OnContentClickedListener
+import org.linphone.activities.main.viewmodels.ListTopBarViewModel
+import org.linphone.core.ChatMessage
+import org.linphone.core.ChatRoomCapabilities
+import org.linphone.core.EventLog
+import org.linphone.databinding.ChatEventListCellBinding
+import org.linphone.databinding.ChatMessageListCellBinding
+import org.linphone.utils.Event
+import org.linphone.utils.LifecycleListAdapter
+import org.linphone.utils.LifecycleViewHolder
+
+class ChatMessagesListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter(ChatMessageDiffCallback()) {
+ companion object {
+ const val MAX_TIME_TO_GROUP_MESSAGES = 300 // 5 minutes
+ }
+
+ val resendMessageEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val deleteMessageEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val forwardMessageEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val showImdnForMessageEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val addSipUriToContactEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val openContentEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ private val contentClickedListener = object : OnContentClickedListener {
+ override fun onContentClicked(path: String) {
+ openContentEvent.value = Event(path)
+ }
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LifecycleViewHolder {
+ 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
+ )
+ val viewHolder = ChatMessageViewHolder(binding)
+ binding.lifecycleOwner = viewHolder
+ return viewHolder
+ }
+
+ private fun createEventViewHolder(parent: ViewGroup): EventViewHolder {
+ val binding: ChatEventListCellBinding = DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context),
+ R.layout.chat_event_list_cell, parent, false
+ )
+ val viewHolder = EventViewHolder(binding)
+ binding.lifecycleOwner = viewHolder
+ return viewHolder
+ }
+
+ override fun onBindViewHolder(holder: LifecycleViewHolder, 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.type.toInt()
+ }
+
+ inner class ChatMessageViewHolder(
+ private val binding: ChatMessageListCellBinding
+ ) : LifecycleViewHolder(binding), PopupMenu.OnMenuItemClickListener {
+ fun bind(eventLog: EventLog) {
+ with(binding) {
+ if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
+ val chatMessage = eventLog.chatMessage
+ val chatMessageViewModel = ChatMessageViewModel(chatMessage, contentClickedListener)
+ viewModel = chatMessageViewModel
+
+ // This is for item selection through ListTopBarFragment
+ selectionListViewModel = selectionViewModel
+ selectionViewModel.isEditionEnabled.observe(this@ChatMessageViewHolder, Observer {
+ position = adapterPosition
+ })
+
+ binding.setClickListener {
+ if (selectionViewModel.isEditionEnabled.value == true) {
+ selectionViewModel.onToggleSelect(adapterPosition)
+ }
+ }
+
+ // Grouping
+ var hasPrevious = false
+ var hasNext = false
+
+ if (adapterPosition > 0) {
+ val previousItem = getItem(adapterPosition - 1)
+ if (previousItem.type == EventLog.Type.ConferenceChatMessage) {
+ val previousMessage = previousItem.chatMessage
+ if (previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
+ if (chatMessage.time - previousMessage.time < MAX_TIME_TO_GROUP_MESSAGES) {
+ hasPrevious = true
+ }
+ }
+ }
+ }
+
+ if (adapterPosition >= 0 && adapterPosition < itemCount - 1) {
+ val nextItem = getItem(adapterPosition + 1)
+ if (nextItem.type == EventLog.Type.ConferenceChatMessage) {
+ val nextMessage = nextItem.chatMessage
+ if (nextMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
+ if (nextMessage.time - chatMessage.time < MAX_TIME_TO_GROUP_MESSAGES) {
+ hasNext = true
+ }
+ }
+ }
+ }
+
+ chatMessageViewModel.updateBubbleBackground(hasPrevious, hasNext)
+
+ executePendingBindings()
+
+ setContextMenuClickListener {
+ val popup = PopupMenu(root.context, background)
+ popup.setOnMenuItemClickListener(this@ChatMessageViewHolder)
+ popup.inflate(R.menu.chat_message_menu)
+
+ if (!chatMessage.isOutgoing ||
+ chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()) ||
+ chatMessage.state == ChatMessage.State.NotDelivered) { // No message id
+ popup.menu.removeItem(R.id.chat_message_menu_imdn_infos)
+ }
+ if (chatMessage.state != ChatMessage.State.NotDelivered) {
+ popup.menu.removeItem(R.id.chat_message_menu_resend)
+ }
+ if (!chatMessage.hasTextContent()) {
+ popup.menu.removeItem(R.id.chat_message_menu_copy_text)
+ }
+ if (chatMessageViewModel.contact.value != null) {
+ popup.menu.removeItem(R.id.chat_message_menu_add_to_contacts)
+ }
+
+ popup.show()
+ true
+ }
+ }
+ }
+ }
+
+ override fun onMenuItemClick(item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.chat_message_menu_imdn_infos -> {
+ showImdnDeliveryFragment()
+ true
+ }
+ R.id.chat_message_menu_resend -> {
+ resendMessage()
+ true
+ }
+ R.id.chat_message_menu_copy_text -> {
+ copyTextToClipboard()
+ true
+ }
+ R.id.chat_message_forward_message -> {
+ forwardMessage()
+ true
+ }
+ R.id.chat_message_menu_delete_message -> {
+ deleteMessage()
+ true
+ }
+ R.id.chat_message_menu_add_to_contacts -> {
+ addSenderToContacts()
+ true
+ }
+ else -> false
+ }
+ }
+
+ private fun resendMessage() {
+ val chatMessage = binding.viewModel?.chatMessage
+ if (chatMessage != null) {
+ val viewHolder = binding.lifecycleOwner as ChatMessageViewHolder
+ chatMessage.userData = viewHolder.adapterPosition
+ resendMessageEvent.value = Event(chatMessage)
+ }
+ }
+
+ private fun copyTextToClipboard() {
+ val chatMessage = binding.viewModel?.chatMessage
+ if (chatMessage != null && chatMessage.hasTextContent()) {
+ val clipboard: ClipboardManager = coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
+ val clip = ClipData.newPlainText("Message", chatMessage.textContent)
+ clipboard.setPrimaryClip(clip)
+ }
+ }
+
+ private fun forwardMessage() {
+ val chatMessage = binding.viewModel?.chatMessage
+ if (chatMessage != null) {
+ forwardMessageEvent.value = Event(chatMessage)
+ }
+ }
+
+ private fun showImdnDeliveryFragment() {
+ val chatMessage = binding.viewModel?.chatMessage
+ if (chatMessage != null) {
+ showImdnForMessageEvent.value = Event(chatMessage)
+ }
+ }
+
+ private fun deleteMessage() {
+ val chatMessage = binding.viewModel?.chatMessage
+ if (chatMessage != null) {
+ val viewHolder = binding.lifecycleOwner as ChatMessageViewHolder
+ chatMessage.userData = viewHolder.adapterPosition
+ deleteMessageEvent.value = Event(chatMessage)
+ }
+ }
+
+ private fun addSenderToContacts() {
+ val chatMessage = binding.viewModel?.chatMessage
+ if (chatMessage != null) {
+ chatMessage.fromAddress.clean() // To remove gruu if any
+ addSipUriToContactEvent.value = Event(chatMessage.fromAddress.asStringUriOnly())
+ }
+ }
+ }
+
+ inner class EventViewHolder(
+ private val binding: ChatEventListCellBinding
+ ) : LifecycleViewHolder(binding) {
+ fun bind(eventLog: EventLog) {
+ with(binding) {
+ val eventViewModel = EventViewModel(eventLog)
+ viewModel = eventViewModel
+
+ // This is for item selection through ListTopBarFragment
+ selectionListViewModel = selectionViewModel
+ selectionViewModel.isEditionEnabled.observe(this@EventViewHolder, Observer {
+ position = adapterPosition
+ })
+
+ binding.setClickListener {
+ if (selectionViewModel.isEditionEnabled.value == true) {
+ selectionViewModel.onToggleSelect(adapterPosition)
+ }
+ }
+
+ executePendingBindings()
+ }
+ }
+ }
+}
+
+private class ChatMessageDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: EventLog,
+ newItem: EventLog
+ ): Boolean {
+ return if (oldItem.type == EventLog.Type.ConferenceChatMessage &&
+ newItem.type == EventLog.Type.ConferenceChatMessage) {
+ oldItem.chatMessage.time == newItem.chatMessage.time &&
+ oldItem.chatMessage.isOutgoing == newItem.chatMessage.isOutgoing
+ } else oldItem.notifyId == newItem.notifyId
+ }
+
+ override fun areContentsTheSame(
+ oldItem: EventLog,
+ newItem: EventLog
+ ): Boolean {
+ return if (newItem.type == EventLog.Type.ConferenceChatMessage) {
+ newItem.chatMessage.state == ChatMessage.State.Displayed
+ } else false
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt
new file mode 100644
index 000000000..c16d9ffa0
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomCreationContactsAdapter.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DiffUtil
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationContactViewModel
+import org.linphone.core.Address
+import org.linphone.core.FriendCapability
+import org.linphone.core.SearchResult
+import org.linphone.databinding.ChatRoomCreationContactCellBinding
+import org.linphone.utils.Event
+import org.linphone.utils.LifecycleListAdapter
+import org.linphone.utils.LifecycleViewHolder
+
+class ChatRoomCreationContactsAdapter : LifecycleListAdapter(SearchResultDiffCallback()) {
+ val selectedContact = MutableLiveData>()
+
+ val selectedAddresses = MutableLiveData>()
+
+ var groupChatEnabled: Boolean = false
+
+ val securityEnabled = MutableLiveData()
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
+ val binding: ChatRoomCreationContactCellBinding = DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context),
+ R.layout.chat_room_creation_contact_cell, parent, false
+ )
+ val viewHolder = ViewHolder(binding)
+ binding.lifecycleOwner = viewHolder
+ return viewHolder
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ inner class ViewHolder(
+ private val binding: ChatRoomCreationContactCellBinding
+ ) : LifecycleViewHolder(binding) {
+ fun bind(searchResult: SearchResult) {
+ with(binding) {
+ val searchResultViewModel = ChatRoomCreationContactViewModel(searchResult)
+ viewModel = searchResultViewModel
+
+ securityEnabled.observe(this@ViewHolder, Observer {
+ updateSecurity(searchResult, searchResultViewModel, it)
+ })
+
+ selectedAddresses.observe(this@ViewHolder, Observer {
+ val selected = it.find { address ->
+ if (searchResult.address != null) address.weakEqual(searchResult.address) else false
+ }
+ searchResultViewModel.isSelected.value = selected != null
+ })
+
+ setClickListener {
+ selectedContact.value = Event(searchResult)
+ }
+
+ executePendingBindings()
+ }
+ }
+
+ private fun updateSecurity(
+ searchResult: SearchResult,
+ viewModel: ChatRoomCreationContactViewModel,
+ securityEnabled: Boolean
+ ) {
+ val isMyself = securityEnabled && searchResult.address != null && coreContext.core.defaultProxyConfig?.identityAddress?.weakEqual(searchResult.address) ?: false
+ val limeCheck = !securityEnabled || (securityEnabled && searchResult.hasCapability(FriendCapability.LimeX3Dh))
+ val groupCheck = !groupChatEnabled || (groupChatEnabled && searchResult.hasCapability(FriendCapability.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 {
+ return if (oldItem.address != null && newItem.address != null) oldItem.address.weakEqual(newItem.address) else false
+ }
+
+ override fun areContentsTheSame(
+ oldItem: SearchResult,
+ newItem: SearchResult
+ ): Boolean {
+ return newItem.friend != null
+ }
+}
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
new file mode 100644
index 000000000..0c0786bf1
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatRoomsListAdapter.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.MutableLiveData
+import androidx.lifecycle.Observer
+import androidx.recyclerview.widget.DiffUtil
+import org.linphone.R
+import org.linphone.activities.main.chat.viewmodels.ChatRoomViewModel
+import org.linphone.activities.main.viewmodels.ListTopBarViewModel
+import org.linphone.core.ChatRoom
+import org.linphone.databinding.ChatRoomListCellBinding
+import org.linphone.utils.Event
+import org.linphone.utils.LifecycleListAdapter
+import org.linphone.utils.LifecycleViewHolder
+
+class ChatRoomsListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter(ChatRoomDiffCallback()) {
+ val selectedChatRoomEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ 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
+ )
+ val viewHolder = ViewHolder(binding)
+ binding.lifecycleOwner = viewHolder
+ return viewHolder
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ inner class ViewHolder(
+ private val binding: ChatRoomListCellBinding
+ ) : LifecycleViewHolder(binding) {
+ fun bind(chatRoom: ChatRoom) {
+ with(binding) {
+ val chatRoomViewModel = ChatRoomViewModel(chatRoom)
+ viewModel = chatRoomViewModel
+
+ // This is for item selection through ListTopBarFragment
+ selectionListViewModel = selectionViewModel
+ selectionViewModel.isEditionEnabled.observe(this@ViewHolder, Observer {
+ position = adapterPosition
+ })
+
+ binding.setClickListener {
+ if (selectionViewModel.isEditionEnabled.value == true) {
+ selectionViewModel.onToggleSelect(adapterPosition)
+ } else {
+ selectedChatRoomEvent.value = Event(chatRoom)
+ chatRoom.markAsRead()
+ }
+ }
+
+ executePendingBindings()
+ }
+ }
+ }
+}
+
+private class ChatRoomDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ChatRoom,
+ newItem: ChatRoom
+ ): Boolean {
+ return oldItem.localAddress.weakEqual(newItem.localAddress) &&
+ oldItem.peerAddress.weakEqual(newItem.peerAddress)
+ }
+
+ override fun areContentsTheSame(
+ oldItem: ChatRoom,
+ newItem: ChatRoom
+ ): Boolean {
+ return newItem.unreadMessagesCount == 0
+ }
+}
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
new file mode 100644
index 000000000..b5af4b208
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/GroupInfoParticipantsAdapter.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.MutableLiveData
+import androidx.recyclerview.widget.DiffUtil
+import org.linphone.R
+import org.linphone.activities.main.chat.GroupChatRoomMember
+import org.linphone.activities.main.chat.viewmodels.GroupInfoParticipantViewModel
+import org.linphone.databinding.ChatRoomGroupInfoParticipantCellBinding
+import org.linphone.utils.Event
+import org.linphone.utils.LifecycleListAdapter
+import org.linphone.utils.LifecycleViewHolder
+
+class GroupInfoParticipantsAdapter(private val isEncryptionEnabled: Boolean) : LifecycleListAdapter(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
+ )
+ val viewHolder = ViewHolder(binding)
+ binding.lifecycleOwner = viewHolder
+ return viewHolder
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ fun showAdminControls(show: Boolean) {
+ showAdmin = show
+ notifyDataSetChanged()
+ }
+
+ inner class ViewHolder(
+ private val binding: ChatRoomGroupInfoParticipantCellBinding
+ ) : LifecycleViewHolder(binding) {
+ fun bind(participant: GroupChatRoomMember) {
+ with(binding) {
+ val participantViewModel = GroupInfoParticipantViewModel(participant)
+ participantViewModel.showAdminControls.value = showAdmin
+ viewModel = participantViewModel
+
+ setRemoveClickListener {
+ participantRemovedEvent.value = Event(participant)
+ }
+ isEncrypted = isEncryptionEnabled
+
+ executePendingBindings()
+ }
+ }
+ }
+}
+
+private class ParticipantDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: GroupChatRoomMember,
+ newItem: GroupChatRoomMember
+ ): Boolean {
+ return oldItem.address.weakEqual(newItem.address)
+ }
+
+ override fun areContentsTheSame(
+ oldItem: GroupChatRoomMember,
+ newItem: GroupChatRoomMember
+ ): 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
new file mode 100644
index 000000000..0100ab227
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ImdnAdapter.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import org.linphone.R
+import org.linphone.activities.main.chat.viewmodels.ImdnParticipantViewModel
+import org.linphone.core.ChatMessage
+import org.linphone.core.ParticipantImdnState
+import org.linphone.databinding.ChatRoomImdnParticipantCellBinding
+import org.linphone.databinding.ImdnListHeaderBinding
+import org.linphone.utils.HeaderAdapter
+import org.linphone.utils.LifecycleViewHolder
+
+class ImdnAdapter : 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
+ )
+ val viewHolder = ViewHolder(binding)
+ binding.lifecycleOwner = viewHolder
+ return viewHolder
+ }
+
+ override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+ holder.bind(getItem(position))
+ }
+
+ inner class ViewHolder(
+ private val binding: ChatRoomImdnParticipantCellBinding
+ ) : LifecycleViewHolder(binding) {
+ fun bind(participantImdnState: ParticipantImdnState) {
+ with(binding) {
+ val imdnViewModel = ImdnParticipantViewModel(participantImdnState)
+ viewModel = imdnViewModel
+
+ 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).state != participantImdnState.state
+ } else true
+ }
+
+ override fun getHeaderViewForPosition(context: Context, position: Int): View {
+ val participantImdnState = getItem(position)
+ 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.message_read
+ }
+ ChatMessage.State.DeliveredToUser -> {
+ binding.title = R.string.chat_message_imdn_delivered
+ binding.textColor = R.color.grey_color
+ binding.icon = R.drawable.message_delivered
+ }
+ ChatMessage.State.Delivered -> {
+ binding.title = R.string.chat_message_imdn_sent
+ binding.textColor = R.color.grey_color
+ binding.icon = R.drawable.message_delivered
+ }
+ ChatMessage.State.NotDelivered -> {
+ binding.title = R.string.chat_message_imdn_undelivered
+ binding.textColor = R.color.red_color
+ binding.icon = R.drawable.message_undelivered
+ }
+ }
+ binding.executePendingBindings()
+ return binding.root
+ }
+}
+
+private class ParticipantImdnStateDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: ParticipantImdnState,
+ newItem: ParticipantImdnState
+ ): Boolean {
+ return oldItem.participant.address.weakEqual(newItem.participant.address)
+ }
+
+ override fun areContentsTheSame(
+ oldItem: ParticipantImdnState,
+ newItem: ParticipantImdnState
+ ): Boolean {
+ return false
+ }
+}
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
new file mode 100644
index 000000000..4f1285931
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatRoomCreationFragment.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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 android.widget.SearchView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import org.linphone.R
+import org.linphone.activities.main.MainActivity
+import org.linphone.activities.main.chat.adapters.ChatRoomCreationContactsAdapter
+import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationViewModel
+import org.linphone.activities.main.viewmodels.SharedMainViewModel
+import org.linphone.core.Address
+import org.linphone.databinding.ChatRoomCreationFragmentBinding
+
+class ChatRoomCreationFragment : Fragment() {
+ private lateinit var binding: ChatRoomCreationFragmentBinding
+ private lateinit var viewModel: ChatRoomCreationViewModel
+ private lateinit var sharedViewModel: SharedMainViewModel
+ private lateinit var adapter: ChatRoomCreationContactsAdapter
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = ChatRoomCreationFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedMainViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ val createGroup = arguments?.getBoolean("createGroup") ?: false
+
+ viewModel = ViewModelProvider(this).get(ChatRoomCreationViewModel::class.java)
+ viewModel.createGroupChat.value = createGroup
+
+ viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
+
+ binding.viewModel = viewModel
+
+ adapter = ChatRoomCreationContactsAdapter()
+ adapter.groupChatEnabled = viewModel.createGroupChat.value == true
+ adapter.securityEnabled.value = viewModel.isEncrypted.value == true
+ binding.contactsList.adapter = adapter
+
+ val layoutManager = LinearLayoutManager(activity)
+ binding.contactsList.layoutManager = layoutManager
+
+ // Divider between items
+ val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation)
+ dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null))
+ binding.contactsList.addItemDecoration(dividerItemDecoration)
+
+ binding.setBackClickListener {
+ findNavController().popBackStack()
+ }
+ binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE
+
+ binding.setAllContactsToggleClickListener {
+ viewModel.sipContactsSelected.value = false
+ }
+
+ binding.setSipContactsToggleClickListener {
+ viewModel.sipContactsSelected.value = true
+ }
+
+ viewModel.contactsList.observe(viewLifecycleOwner, Observer {
+ adapter.submitList(it)
+ })
+
+ viewModel.isEncrypted.observe(viewLifecycleOwner, Observer {
+ adapter.securityEnabled.value = it
+ })
+
+ viewModel.sipContactsSelected.observe(viewLifecycleOwner, Observer {
+ viewModel.updateContactsList()
+ })
+
+ viewModel.selectedAddresses.observe(viewLifecycleOwner, Observer {
+ adapter.selectedAddresses.value = it
+ })
+
+ viewModel.chatRoomCreatedEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { chatRoom ->
+ sharedViewModel.selectedChatRoom.value = chatRoom
+ if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) {
+ findNavController().navigate(R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment)
+ }
+ }
+ })
+
+ adapter.selectedContact.observe(viewLifecycleOwner, Observer {
+ it.consume { searchResult ->
+ if (createGroup) {
+ viewModel.toggleSelectionForSearchResult(searchResult)
+ } else {
+ viewModel.createOneToOneChat(searchResult)
+ }
+ }
+ })
+
+ addParticipantsFromBundle()
+
+ // Next button is only used to go to group chat info fragment
+ binding.setNextClickListener {
+ sharedViewModel.createEncryptedChatRoom = viewModel.isEncrypted.value == true
+
+ if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) {
+ val args = Bundle()
+ args.putSerializable("participants", viewModel.selectedAddresses.value)
+ findNavController().navigate(R.id.action_chatRoomCreationFragment_to_groupInfoFragment, args)
+ }
+ }
+
+ binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
+ override fun onQueryTextSubmit(query: String?): Boolean {
+ return true
+ }
+
+ override fun onQueryTextChange(newText: String?): Boolean {
+ viewModel.filter(newText ?: "")
+ return true
+ }
+ })
+
+ viewModel.onErrorEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { messageResourceId ->
+ (activity as MainActivity).showSnackBar(messageResourceId)
+ }
+ })
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun addParticipantsFromBundle() {
+ val participants = arguments?.getSerializable("participants") as? ArrayList
+ 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
new file mode 100644
index 000000000..561e92cc4
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
@@ -0,0 +1,565 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.ActivityNotFoundException
+import android.content.Intent
+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.webkit.MimeTypeMap
+import androidx.appcompat.view.menu.MenuBuilder
+import androidx.appcompat.view.menu.MenuPopupHelper
+import androidx.core.content.FileProvider
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.Navigation
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import java.io.File
+import kotlinx.android.synthetic.main.tabs_fragment.*
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.LinphoneApplication.Companion.corePreferences
+import org.linphone.R
+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.viewmodels.*
+import org.linphone.activities.main.fragments.MasterFragment
+import org.linphone.activities.main.viewmodels.DialogViewModel
+import org.linphone.activities.main.viewmodels.SharedMainViewModel
+import org.linphone.core.*
+import org.linphone.core.tools.Log
+import org.linphone.databinding.ChatRoomDetailFragmentBinding
+import org.linphone.utils.*
+import org.linphone.utils.Event
+
+class DetailChatRoomFragment : MasterFragment() {
+ private lateinit var binding: ChatRoomDetailFragmentBinding
+ private lateinit var viewModel: ChatRoomViewModel
+ private lateinit var chatSendingViewModel: ChatMessageSendingViewModel
+ private lateinit var listViewModel: ChatMessagesListViewModel
+ private lateinit var adapter: ChatMessagesListAdapter
+ private lateinit var sharedViewModel: SharedMainViewModel
+ private var chatRoomAddress: String? = null
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = ChatRoomDetailFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedMainViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ val chatRoom = sharedViewModel.selectedChatRoom.value
+ chatRoom ?: return
+ chatRoomAddress = chatRoom.peerAddress.asStringUriOnly()
+
+ 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)
+ // SubmitList is done on a background thread
+ // We need this adapter data observer to know when to scroll
+ adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+ override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+ if (positionStart == adapter.itemCount - 1) {
+ adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
+ scrollToBottom()
+ }
+ }
+ })
+ binding.chatMessagesList.adapter = adapter
+
+ val layoutManager = LinearLayoutManager(activity)
+ layoutManager.stackFromEnd = true
+ binding.chatMessagesList.layoutManager = layoutManager
+
+ val chatScrollListener: ChatScrollListener = object : ChatScrollListener(layoutManager) {
+ override fun onLoadMore(totalItemsCount: Int) {
+ listViewModel.loadMoreData(totalItemsCount)
+ }
+ }
+ binding.chatMessagesList.addOnScrollListener(chatScrollListener)
+
+ listViewModel.events.observe(viewLifecycleOwner, Observer { events ->
+ adapter.submitList(events)
+ })
+
+ listViewModel.messageUpdatedEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { position ->
+ adapter.notifyItemChanged(position)
+ }
+ })
+
+ listViewModel.requestWriteExternalStoragePermissionEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
+ }
+ })
+
+ adapter.deleteMessageEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { chatMessage ->
+ listViewModel.deleteMessage(chatMessage)
+ }
+ })
+
+ adapter.resendMessageEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { chatMessage ->
+ listViewModel.resendMessage(chatMessage)
+ }
+ })
+
+ adapter.forwardMessageEvent.observe(viewLifecycleOwner, Observer {
+ 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)
+
+ val deepLink = "linphone-android://chat/"
+ Log.i("[Chat Room] Forwarding message, starting deep link: $deepLink")
+ findNavController().navigate(Uri.parse(deepLink))
+ }
+ })
+
+ adapter.showImdnForMessageEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { chatMessage ->
+ val args = Bundle()
+ args.putString("MessageId", chatMessage.messageId)
+ Navigation.findNavController(binding.root).navigate(R.id.action_detailChatRoomFragment_to_imdnFragment, args)
+ }
+ })
+
+ adapter.addSipUriToContactEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { sipUri ->
+ val deepLink = "linphone-android://contact/new/$sipUri"
+ Log.i("[Chat Room] Creating contact, starting deep link: $deepLink")
+ findNavController().navigate(Uri.parse(deepLink))
+ }
+ })
+
+ adapter.openContentEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { path ->
+ openFile(path)
+ }
+ })
+
+ binding.setBackClickListener {
+ findNavController().popBackStack()
+ }
+ binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE
+
+ binding.setTitleClickListener {
+ binding.sipUri.visibility = if (!viewModel.oneToOneChatRoom ||
+ binding.sipUri.visibility == View.VISIBLE) View.GONE else View.VISIBLE
+ }
+
+ binding.setMenuClickListener {
+ showPopupMenu(chatRoom)
+ }
+
+ binding.setEditClickListener {
+ enterEditionMode()
+ }
+
+ binding.setSecurityIconClickListener {
+ showParticipantsDevices()
+ }
+
+ binding.setAttachFileClickListener {
+ if (PermissionHelper.get().hasReadExternalStorage() && PermissionHelper.get().hasCameraPermission()) {
+ pickFile()
+ } else {
+ Log.i("[Chat Room] Asking for READ_EXTERNAL_STORAGE and CAMERA permissions")
+ requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.CAMERA), 0)
+ }
+ }
+
+ binding.setSendMessageClickListener {
+ chatSendingViewModel.sendMessage()
+ binding.message.text?.clear()
+ }
+
+ binding.setStartCallClickListener {
+ coreContext.startCall(viewModel.addressToCall)
+ }
+
+ sharedViewModel.filesToShare.observe(viewLifecycleOwner, Observer {
+ if (it.isNotEmpty()) {
+ for (path in it) {
+ Log.i("[Chat Room] Found $path file to share")
+ chatSendingViewModel.addAttachment(path)
+ }
+ sharedViewModel.filesToShare.value = arrayListOf()
+ }
+ })
+
+ sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { chatMessage ->
+ Log.i("[Chat Room] Found message to transfer")
+ showForwardConfirmationDialog(chatMessage)
+ }
+ })
+ }
+
+ override fun getItemCount(): Int {
+ return adapter.itemCount
+ }
+
+ override fun deleteItems(indexesOfItemToDelete: ArrayList) {
+ val list = ArrayList()
+ for (index in indexesOfItemToDelete) {
+ val eventLog = adapter.getItemAt(index)
+ list.add(eventLog)
+ }
+ listViewModel.deleteEventLogs(list)
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ if (requestCode == 0) {
+ var atLeastOneGranted = false
+ for (result in grantResults) {
+ atLeastOneGranted = atLeastOneGranted || result == PackageManager.PERMISSION_GRANTED
+ }
+ if (atLeastOneGranted) {
+ pickFile()
+ }
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ // Prevent notifications for this chat room to be displayed
+ coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = chatRoomAddress
+ scrollToBottom()
+ }
+
+ override fun onPause() {
+ coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
+
+ super.onPause()
+ }
+
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (resultCode == Activity.RESULT_OK) {
+ var fileToUploadPath: String? = null
+
+ val temporaryFileUploadPath = chatSendingViewModel.temporaryFileUploadPath
+ if (temporaryFileUploadPath != null) {
+ if (data != null) {
+ val dataUri = data.data
+ if (dataUri != null) {
+ fileToUploadPath = dataUri.toString()
+ Log.i("[Chat Room] Using data URI $fileToUploadPath")
+ } else if (temporaryFileUploadPath.exists()) {
+ fileToUploadPath = temporaryFileUploadPath.absolutePath
+ Log.i("[Chat Room] Data URI is null, using $fileToUploadPath")
+ }
+ } else if (temporaryFileUploadPath.exists()) {
+ fileToUploadPath = temporaryFileUploadPath.absolutePath
+ Log.i("[Chat Room] Data is null, using $fileToUploadPath")
+ }
+ }
+
+ if (fileToUploadPath != null) {
+ if (fileToUploadPath.startsWith("content://") ||
+ fileToUploadPath.startsWith("file://")
+ ) {
+ val uriToParse = Uri.parse(fileToUploadPath)
+ fileToUploadPath = FileUtils.getFilePath(requireContext(), uriToParse)
+ Log.i("[Chat] Path was using a content or file scheme, real path is: $fileToUploadPath")
+ if (fileToUploadPath == null) {
+ Log.e("[Chat] Failed to get access to file $uriToParse")
+ }
+ }
+ }
+
+ if (fileToUploadPath != null) {
+ chatSendingViewModel.addAttachment(fileToUploadPath)
+ }
+ }
+ }
+
+ 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
+
+ if (viewModel.oneParticipantOneDevice) {
+ coreContext.startCall(viewModel.onlyParticipantOnlyDeviceAddress, true)
+ } else {
+ if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
+ findNavController().navigate(R.id.action_detailChatRoomFragment_to_devicesFragment)
+ }
+ }
+
+ dialog.dismiss()
+ }, okLabel)
+
+ dialog.show()
+ } else {
+ if (viewModel.oneParticipantOneDevice) {
+ coreContext.startCall(viewModel.onlyParticipantOnlyDeviceAddress, true)
+ } else {
+ if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
+ findNavController().navigate(R.id.action_detailChatRoomFragment_to_devicesFragment)
+ }
+ }
+ }
+ }
+
+ private fun showGroupInfo(chatRoom: ChatRoom) {
+ sharedViewModel.selectedGroupChatRoom.value = chatRoom
+ if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
+ findNavController().navigate(R.id.action_detailChatRoomFragment_to_groupInfoFragment)
+ }
+ }
+
+ private fun showEphemeralMessages() {
+ if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
+ findNavController().navigate(R.id.action_detailChatRoomFragment_to_ephemeralFragment)
+ }
+ }
+
+ private fun showForwardConfirmationDialog(chatMessage: ChatMessage) {
+ val viewModel = DialogViewModel(getString(R.string.chat_message_forward_confirmation_dialog))
+ viewModel.iconResource = R.drawable.forward_message_dialog_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 builder = MenuBuilder(requireContext())
+ val popupMenu = MenuPopupHelper(requireContext(), builder, binding.menu)
+ popupMenu.setForceShowIcon(true)
+
+ MenuInflater(requireContext()).inflate(R.menu.chat_room_menu, builder)
+ if (viewModel.oneToOneChatRoom) {
+ builder.removeItem(R.id.chat_room_group_info)
+
+ // 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) {
+ builder.removeItem(R.id.chat_room_participants_devices)
+ }
+ }
+ if (!viewModel.encryptedChatRoom) {
+ builder.removeItem(R.id.chat_room_participants_devices)
+ builder.removeItem(R.id.chat_room_ephemeral_messages)
+ }
+ // TODO: hide ephemeral menu if not all participants support the feature
+
+ builder.setCallback(object : MenuBuilder.Callback {
+ override fun onMenuModeChange(menu: MenuBuilder?) {}
+
+ override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
+ return when (item.itemId) {
+ R.id.chat_room_group_info -> {
+ showGroupInfo(chatRoom)
+ true
+ }
+ R.id.chat_room_participants_devices -> {
+ showParticipantsDevices()
+ true
+ }
+ R.id.chat_room_ephemeral_messages -> {
+ showEphemeralMessages()
+ true
+ }
+ R.id.chat_room_delete_messages -> {
+ enterEditionMode()
+ true
+ }
+ else -> false
+ }
+ }
+ })
+
+ popupMenu.show()
+ }
+
+ private fun scrollToBottom() {
+ if (adapter.itemCount > 0) {
+ binding.chatMessagesList.scrollToPosition(adapter.itemCount - 1)
+ }
+ }
+
+ private fun pickFile() {
+ val cameraIntents = ArrayList()
+
+ // Handles image & video picking
+ val galleryIntent = Intent(Intent.ACTION_PICK)
+ galleryIntent.type = "*/*"
+ galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
+
+ if (PermissionHelper.get().hasCameraPermission()) {
+ // Allows to capture directly from the camera
+ val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+ val tempFileName = System.currentTimeMillis().toString() + ".jpeg"
+ chatSendingViewModel.temporaryFileUploadPath =
+ FileUtils.getFileStoragePath(tempFileName)
+ val uri = Uri.fromFile(chatSendingViewModel.temporaryFileUploadPath)
+ captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
+ cameraIntents.add(captureIntent)
+ }
+
+ if (PermissionHelper.get().hasReadExternalStorage()) {
+ // Finally allow any kind of file
+ val fileIntent = Intent(Intent.ACTION_GET_CONTENT)
+ fileIntent.type = "*/*"
+ cameraIntents.add(fileIntent)
+ }
+
+ 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 openFile(contentFilePath: String) {
+ val intent = Intent(Intent.ACTION_VIEW)
+ val contentUri: Uri
+ var path = contentFilePath
+
+ when {
+ path.startsWith("file://") -> {
+ path = path.substring("file://".length)
+ val file = File(path)
+ contentUri = FileProvider.getUriForFile(
+ requireContext(),
+ getString(R.string.file_provider),
+ file
+ )
+ }
+ path.startsWith("content://") -> {
+ contentUri = Uri.parse(path)
+ }
+ else -> {
+ val file = File(path)
+ contentUri = try {
+ FileProvider.getUriForFile(
+ requireContext(),
+ getString(R.string.file_provider),
+ file
+ )
+ } catch (e: Exception) {
+ Log.e(
+ "[Chat Message] Couldn't get URI for file $file using file provider ${getString(R.string.file_provider)}"
+ )
+ Uri.parse(path)
+ }
+ }
+ }
+
+ val filePath: String = contentUri.toString()
+ Log.i("[Chat Message] Trying to open file: $filePath")
+ var type: String? = null
+ val extension = FileUtils.getExtensionFromFileName(filePath)
+
+ if (extension.isNotEmpty()) {
+ Log.i("[Chat Message] Found extension $extension")
+ type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
+ } else {
+ Log.e("[Chat Message] Couldn't find extension")
+ }
+
+ if (type != null) {
+ Log.i("[Chat Message] Found matching MIME type $type")
+ } else {
+ type = FileUtils.getMimeFromFile(filePath)
+ Log.e("[Chat Message] Can't get MIME type from extension: $extension, will use $type")
+ }
+
+ intent.setDataAndType(contentUri, type)
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+
+ try {
+ startActivity(intent)
+ } catch (anfe: ActivityNotFoundException) {
+ Log.e("[Chat Message] Couldn't find an activity to handle MIME type: $type")
+ val activity = requireActivity() as MainActivity
+ activity.showSnackBar(R.string.chat_room_cant_open_file_no_app_found)
+ }
+ }
+}
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
new file mode 100644
index 000000000..8bff9f38b
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DevicesFragment.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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 androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.activities.main.chat.viewmodels.DevicesListViewModel
+import org.linphone.activities.main.chat.viewmodels.DevicesListViewModelFactory
+import org.linphone.activities.main.viewmodels.SharedMainViewModel
+import org.linphone.databinding.ChatRoomDevicesFragmentBinding
+
+class DevicesFragment : Fragment() {
+ private lateinit var binding: ChatRoomDevicesFragmentBinding
+ private lateinit var listViewModel: DevicesListViewModel
+ private lateinit var sharedViewModel: SharedMainViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = ChatRoomDevicesFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedMainViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ val chatRoom = sharedViewModel.selectedChatRoom.value
+ chatRoom ?: return
+
+ listViewModel = ViewModelProvider(
+ this,
+ DevicesListViewModelFactory(chatRoom)
+ )[DevicesListViewModel::class.java]
+ binding.viewModel = listViewModel
+
+ binding.setBackClickListener {
+ findNavController().popBackStack()
+ }
+ }
+}
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
new file mode 100644
index 000000000..5aa15a1a0
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/EphemeralFragment.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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 androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import org.linphone.activities.main.chat.viewmodels.EphemeralViewModel
+import org.linphone.activities.main.chat.viewmodels.EphemeralViewModelFactory
+import org.linphone.activities.main.viewmodels.SharedMainViewModel
+import org.linphone.databinding.ChatRoomEphemeralFragmentBinding
+
+class EphemeralFragment : Fragment() {
+ private lateinit var binding: ChatRoomEphemeralFragmentBinding
+ private lateinit var viewModel: EphemeralViewModel
+ private lateinit var sharedViewModel: SharedMainViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = ChatRoomEphemeralFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedMainViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ val chatRoom = sharedViewModel.selectedChatRoom.value
+ chatRoom ?: return
+
+ viewModel = ViewModelProvider(
+ this,
+ EphemeralViewModelFactory(chatRoom)
+ )[EphemeralViewModel::class.java]
+ binding.viewModel = viewModel
+
+ binding.setBackClickListener {
+ findNavController().popBackStack()
+ }
+
+ binding.setValidClickListener {
+ viewModel.updateChatRoomEphemeralDuration()
+ findNavController().popBackStack()
+ }
+ }
+}
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
new file mode 100644
index 000000000..2cd186b44
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/GroupInfoFragment.kt
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+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.viewmodels.GroupInfoViewModel
+import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModelFactory
+import org.linphone.activities.main.viewmodels.DialogViewModel
+import org.linphone.activities.main.viewmodels.SharedMainViewModel
+import org.linphone.core.Address
+import org.linphone.core.ChatRoom
+import org.linphone.core.ChatRoomCapabilities
+import org.linphone.databinding.ChatRoomGroupInfoFragmentBinding
+import org.linphone.utils.DialogUtils
+
+class GroupInfoFragment : Fragment() {
+ private lateinit var binding: ChatRoomGroupInfoFragmentBinding
+ private lateinit var viewModel: GroupInfoViewModel
+ private lateinit var sharedViewModel: SharedMainViewModel
+ private lateinit var adapter: GroupInfoParticipantsAdapter
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = ChatRoomGroupInfoFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedMainViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value
+
+ viewModel = ViewModelProvider(
+ this,
+ GroupInfoViewModelFactory(chatRoom)
+ )[GroupInfoViewModel::class.java]
+ binding.viewModel = viewModel
+
+ viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
+
+ adapter = GroupInfoParticipantsAdapter(chatRoom?.hasCapability(ChatRoomCapabilities.Encrypted.toInt()) ?: viewModel.isEncrypted.value == true)
+ binding.participants.adapter = adapter
+
+ val layoutManager = LinearLayoutManager(activity)
+ binding.participants.layoutManager = layoutManager
+
+ // Divider between items
+ val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation)
+ dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null))
+ binding.participants.addItemDecoration(dividerItemDecoration)
+
+ viewModel.participants.observe(viewLifecycleOwner, Observer {
+ adapter.submitList(it)
+ })
+
+ viewModel.isMeAdmin.observe(viewLifecycleOwner, Observer {
+ adapter.showAdminControls(it && chatRoom != null)
+ })
+
+ adapter.participantRemovedEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { participant ->
+ viewModel.removeParticipant(participant)
+ }
+ })
+
+ addParticipantsFromBundle()
+
+ binding.setBackClickListener {
+ findNavController().popBackStack()
+ }
+
+ viewModel.createdChatRoomEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { chatRoom ->
+ sharedViewModel.selectedChatRoom.value = chatRoom
+ goToChatRoom()
+ }
+ })
+
+ binding.setNextClickListener {
+ if (viewModel.chatRoom != null) {
+ viewModel.updateRoom()
+ } else {
+ viewModel.createChatRoom()
+ }
+ }
+
+ binding.setParticipantsClickListener {
+ sharedViewModel.createEncryptedChatRoom = viewModel.isEncrypted.value == true
+
+ if (findNavController().currentDestination?.id == R.id.groupInfoFragment) {
+ val args = Bundle()
+ args.putBoolean("createGroup", true)
+
+ val list = arrayListOf()
+ for (participant in viewModel.participants.value.orEmpty()) {
+ list.add(participant.address)
+ }
+ args.putSerializable("participants", list)
+
+ findNavController().navigate(R.id.action_groupInfoFragment_to_chatRoomCreationFragment, 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.onErrorEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { messageResourceId ->
+ (activity as MainActivity).showSnackBar(messageResourceId)
+ }
+ })
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun addParticipantsFromBundle() {
+ val participants = arguments?.getSerializable("participants") as? ArrayList
+ if (participants != null && participants.size > 0) {
+ val list = arrayListOf()
+
+ for (address in participants) {
+ val exists = viewModel.participants.value?.find {
+ it.address.weakEqual(address)
+ }
+
+ if (exists != null) {
+ list.add(exists)
+ } else {
+ list.add(GroupChatRoomMember(address, false, hasLimeX3DHCapability = viewModel.isEncrypted.value == true))
+ }
+ }
+
+ viewModel.participants.value = list
+ }
+ }
+
+ private fun goToChatRoom() {
+ if (findNavController().currentDestination?.id == R.id.groupInfoFragment) {
+ findNavController().navigate(R.id.action_groupInfoFragment_to_detailChatRoomFragment)
+ }
+ }
+}
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
new file mode 100644
index 000000000..febfabe25
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ImdnFragment.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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 androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.DividerItemDecoration
+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.viewmodels.SharedMainViewModel
+import org.linphone.core.tools.Log
+import org.linphone.databinding.ChatRoomImdnFragmentBinding
+import org.linphone.utils.RecyclerViewHeaderDecoration
+
+class ImdnFragment : Fragment() {
+ private lateinit var binding: ChatRoomImdnFragmentBinding
+ private lateinit var viewModel: ImdnViewModel
+ private lateinit var adapter: ImdnAdapter
+ private lateinit var sharedViewModel: SharedMainViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = ChatRoomImdnFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedMainViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ val chatRoom = sharedViewModel.selectedChatRoom.value
+ chatRoom ?: return
+
+ if (arguments != null) {
+ val messageId = arguments?.getString("MessageId")
+ val message = chatRoom.findMessage(messageId)
+ 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()
+ binding.participantsList.adapter = adapter
+
+ val layoutManager = LinearLayoutManager(activity)
+ binding.participantsList.layoutManager = layoutManager
+
+ // Divider between items
+ val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation)
+ dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null))
+ binding.participantsList.addItemDecoration(dividerItemDecoration)
+
+ // Displays state header
+ val headerItemDecoration = RecyclerViewHeaderDecoration(adapter)
+ binding.participantsList.addItemDecoration(headerItemDecoration)
+
+ viewModel.participants.observe(viewLifecycleOwner, Observer {
+ adapter.submitList(it)
+ })
+
+ binding.setBackClickListener {
+ findNavController().popBackStack()
+ }
+ }
+}
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
new file mode 100644
index 000000000..510a031b6
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/MasterChatRoomsFragment.kt
@@ -0,0 +1,250 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.core.os.bundleOf
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.fragment.findNavController
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+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.main.viewmodels.SharedMainViewModel
+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() {
+ private lateinit var binding: ChatRoomMasterFragmentBinding
+ private lateinit var listViewModel: ChatRoomsListViewModel
+ private lateinit var adapter: ChatRoomsListAdapter
+ private lateinit var sharedViewModel: SharedMainViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = ChatRoomMasterFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ binding.lifecycleOwner = this
+
+ listViewModel = ViewModelProvider(this).get(ChatRoomsListViewModel::class.java)
+ binding.viewModel = listViewModel
+
+ sharedViewModel = activity?.run {
+ ViewModelProvider(this).get(SharedMainViewModel::class.java)
+ } ?: throw Exception("Invalid Activity")
+
+ adapter = ChatRoomsListAdapter(listSelectionViewModel)
+ // SubmitList is done on a background thread
+ // We need this adapter data observer to know when to scroll
+ adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
+ override fun onChanged() {
+ scrollToTop()
+ }
+ override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+ scrollToTop()
+ }
+ override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
+ scrollToTop()
+ }
+ })
+ binding.chatList.adapter = adapter
+
+ val layoutManager = LinearLayoutManager(activity)
+ binding.chatList.layoutManager = layoutManager
+
+ val swipeConfiguration = RecyclerViewSwipeConfiguration()
+ val white = ContextCompat.getColor(requireContext(), R.color.white_color)
+
+ swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action("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.dialog_default_delete_message))
+ val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
+
+ viewModel.showCancelButton {
+ adapter.notifyItemChanged(viewHolder.adapterPosition)
+ dialog.dismiss()
+ }
+
+ viewModel.showDeleteButton({
+ listViewModel.deleteChatRoom(listViewModel.chatRooms.value?.get(viewHolder.adapterPosition))
+ dialog.dismiss()
+ }, getString(R.string.dialog_delete))
+
+ dialog.show()
+ }
+ }
+ RecyclerViewSwipeUtils(ItemTouchHelper.LEFT, swipeConfiguration, swipeListener)
+ .attachToRecyclerView(binding.chatList)
+
+ // Divider between items
+ val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation)
+ dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null))
+ binding.chatList.addItemDecoration(dividerItemDecoration)
+
+ listViewModel.chatRooms.observe(viewLifecycleOwner, Observer { chatRooms ->
+ adapter.submitList(chatRooms)
+ })
+
+ listViewModel.latestUpdatedChatRoomId.observe(viewLifecycleOwner, Observer { position ->
+ adapter.notifyItemChanged(position)
+ })
+
+ listViewModel.contactsUpdatedEvent.observe(viewLifecycleOwner, Observer {
+ it.consume {
+ adapter.notifyDataSetChanged()
+ }
+ })
+
+ adapter.selectedChatRoomEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { chatRoom ->
+ sharedViewModel.selectedChatRoom.value = chatRoom
+ if (!resources.getBoolean(R.bool.isTablet)) {
+ if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) {
+ findNavController().navigate(R.id.action_masterChatRoomsFragment_to_detailChatRoomFragment)
+ }
+ } else {
+ val navHostFragment =
+ childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
+ navHostFragment.navController.navigate(R.id.action_global_detailChatRoomFragment)
+ }
+ }
+ })
+
+ binding.setEditClickListener {
+ listSelectionViewModel.isEditionEnabled.value = true
+ }
+
+ binding.setNewOneToOneChatRoomClickListener {
+ val bundle = bundleOf("createGroup" to false)
+ if (!resources.getBoolean(R.bool.isTablet)) {
+ if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) {
+ findNavController().navigate(
+ R.id.action_masterChatRoomsFragment_to_chatRoomCreationFragment,
+ bundle
+ )
+ }
+ } else {
+ val navHostFragment =
+ childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
+ navHostFragment.navController.navigate(R.id.action_global_chatRoomCreationFragment, bundle)
+ }
+ }
+
+ binding.setNewGroupChatRoomClickListener {
+ sharedViewModel.selectedGroupChatRoom.value = null
+
+ val bundle = bundleOf("createGroup" to true)
+ if (!resources.getBoolean(R.bool.isTablet)) {
+ if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) {
+ findNavController().navigate(
+ R.id.action_masterChatRoomsFragment_to_chatRoomCreationFragment,
+ bundle
+ )
+ }
+ } else {
+ val navHostFragment =
+ childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
+ navHostFragment.navController.navigate(R.id.action_global_chatRoomCreationFragment, bundle)
+ }
+ }
+
+ val localSipUri = arguments?.getString("LocalSipUri")
+ val remoteSipUri = arguments?.getString("RemoteSipUri")
+ if (localSipUri != null && remoteSipUri != null) {
+ Log.i("[Chat] Found local ($localSipUri) & remote addresses ($remoteSipUri) in arguments")
+ arguments?.clear()
+ val localAddress = Factory.instance().createAddress(localSipUri)
+ val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
+ val chatRoom = coreContext.core.getChatRoom(remoteSipAddress, localAddress)
+ if (chatRoom != null) {
+ Log.i("[Chat] Found matching chat room $chatRoom")
+ chatRoom.markAsRead()
+ adapter.selectedChatRoomEvent.value = Event(chatRoom)
+ }
+ } else {
+ sharedViewModel.filesToShare.observe(viewLifecycleOwner, Observer {
+ if (it.isNotEmpty()) {
+ Log.i("[Chat] Found ${it.size} files to share")
+ val activity = requireActivity() as MainActivity
+ activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing)
+ }
+ })
+ sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner, Observer {
+ if (!it.consumed()) {
+ Log.i("[Chat] Found chat message to transfer")
+
+ val activity = requireActivity() as MainActivity
+ activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing)
+ }
+ })
+
+ listViewModel.onErrorEvent.observe(viewLifecycleOwner, Observer {
+ it.consume { messageResourceId ->
+ (activity as MainActivity).showSnackBar(messageResourceId)
+ }
+ })
+ }
+ }
+
+ override fun getItemCount(): Int {
+ return adapter.itemCount
+ }
+
+ override fun deleteItems(indexesOfItemToDelete: ArrayList) {
+ val list = ArrayList()
+ for (index in indexesOfItemToDelete) {
+ val chatRoom = adapter.getItemAt(index)
+ list.add(chatRoom)
+ }
+ listViewModel.deleteChatRooms(list)
+ }
+
+ private fun scrollToTop() {
+ binding.chatList.scrollToPosition(0)
+ }
+}
diff --git a/app/src/main/java/org/linphone/chat/DeviceChildViewHolder.java b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageAttachmentViewModel.kt
similarity index 60%
rename from app/src/main/java/org/linphone/chat/DeviceChildViewHolder.java
rename to app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageAttachmentViewModel.kt
index b0f9e06c5..ad597cf48 100644
--- a/app/src/main/java/org/linphone/chat/DeviceChildViewHolder.java
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageAttachmentViewModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2010-2019 Belledonne Communications SARL.
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
@@ -17,19 +17,19 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.linphone.chat;
+package org.linphone.activities.main.chat.viewmodels
-import android.view.View;
-import android.widget.ImageView;
-import android.widget.TextView;
-import org.linphone.R;
+import androidx.lifecycle.ViewModel
+import org.linphone.utils.FileUtils
-class DeviceChildViewHolder {
- public final TextView deviceName;
- public final ImageView securityLevel;
+class ChatMessageAttachmentViewModel(
+ val path: String,
+ val isImage: Boolean,
+ private val deleteCallback: (attachment: ChatMessageAttachmentViewModel) -> Unit
+) : ViewModel() {
+ val fileName: String = FileUtils.getNameFromFilePath(path)
- public DeviceChildViewHolder(View v) {
- deviceName = v.findViewById(R.id.name);
- securityLevel = v.findViewById(R.id.security_level);
+ fun delete() {
+ deleteCallback(this)
}
}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageContentViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageContentViewModel.kt
new file mode 100644
index 000000000..523856702
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageContentViewModel.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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 org.linphone.core.ChatMessage
+import org.linphone.core.Content
+import org.linphone.core.tools.Log
+import org.linphone.utils.FileUtils
+
+class ChatMessageContentViewModel(
+ val content: Content,
+ private val chatMessage: ChatMessage,
+ private val listener: OnContentClickedListener?
+) : ViewModel() {
+ val isImage = MutableLiveData()
+
+ val downloadable = MutableLiveData()
+
+ val downloadEnabled = MutableLiveData()
+
+ val isAlone: Boolean
+ get() {
+ var count = 0
+ for (content in chatMessage.contents) {
+ if (content.isFileTransfer || content.isFile) {
+ count += 1
+ }
+ }
+ return count == 1
+ }
+
+ init {
+ if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
+ downloadable.value = content.filePath.isEmpty()
+
+ if (content.filePath.isNotEmpty()) {
+ Log.i("[Content] Found displayable content: ${content.filePath}")
+ isImage.value = FileUtils.isExtensionImage(content.filePath)
+ } else {
+ Log.w("[Content] Found content with empty path...")
+ isImage.value = false
+ }
+ } else {
+ Log.i("[Content] Found downloadable content: ${content.name}")
+ downloadable.value = true
+ isImage.value = false
+ }
+
+ downloadEnabled.value = downloadable.value
+ }
+
+ fun download() {
+ if (content.isFileTransfer && (content.filePath == null || content.filePath.isEmpty())) {
+ val file = FileUtils.getFileStoragePath(content.name)
+ content.filePath = file.path
+ downloadEnabled.value = false
+
+ Log.i("[Content] Started downloading ${content.name} into ${content.filePath}")
+ chatMessage.downloadContent(content)
+ }
+ }
+
+ fun openFile() {
+ listener?.onContentClicked(content.filePath)
+ }
+}
+
+interface OnContentClickedListener {
+ fun onContentClicked(path: String)
+}
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
new file mode 100644
index 000000000..f57eb4b7d
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.io.File
+import org.linphone.LinphoneApplication.Companion.corePreferences
+import org.linphone.core.*
+import org.linphone.utils.FileUtils
+
+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 sendMessageEnabled = MutableLiveData()
+
+ val isReadOnly = MutableLiveData()
+
+ var textToSend: String = ""
+ set(value) {
+ sendMessageEnabled.value = value.isNotEmpty() || attachments.value?.isNotEmpty() ?: false
+ if (value.isNotEmpty()) {
+ if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
+ attachFileEnabled.value = false
+ }
+ chatRoom.compose()
+ } else {
+ if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
+ attachFileEnabled.value = attachments.value?.isEmpty() ?: true
+ }
+ }
+ field = value
+ }
+
+ init {
+ attachments.value = arrayListOf()
+
+ attachFileEnabled.value = true
+ sendMessageEnabled.value = false
+ isReadOnly.value = chatRoom.hasBeenLeft()
+ }
+
+ fun addAttachment(path: String) {
+ val list = arrayListOf()
+ list.addAll(attachments.value.orEmpty())
+ list.add(ChatMessageAttachmentViewModel(path, FileUtils.isExtensionImage(path)) {
+ removeAttachment(it)
+ })
+ attachments.value = list
+
+ sendMessageEnabled.value = textToSend.isNotEmpty() || list.isNotEmpty()
+ if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
+ attachFileEnabled.value = false
+ }
+ }
+
+ private fun removeAttachment(attachment: ChatMessageAttachmentViewModel) {
+ val list = arrayListOf()
+ list.addAll(attachments.value.orEmpty())
+ list.remove(attachment)
+ attachments.value = list
+
+ sendMessageEnabled.value = textToSend.isNotEmpty() || list.isNotEmpty()
+ if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
+ attachFileEnabled.value = list.isEmpty()
+ }
+ }
+
+ fun sendMessage() {
+ val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
+ val message: ChatMessage = chatRoom.createEmptyMessage()
+
+ if (textToSend.isNotEmpty()) {
+ message.addTextContent(textToSend)
+ }
+
+ for (attachment in attachments.value.orEmpty()) {
+ val content = Factory.instance().createContent()
+
+ if (attachment.isImage) {
+ content.type = "image"
+ } else {
+ content.type = "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
+
+ if (isBasicChatRoom) {
+ val fileMessage: ChatMessage = chatRoom.createFileTransferMessage(content)
+ fileMessage.send()
+ } else {
+ message.addFileContent(content)
+ }
+ }
+
+ if (message.contents.isNotEmpty()) {
+ message.send()
+ }
+
+ attachments.value = arrayListOf()
+ }
+
+ fun transferMessage(chatMessage: ChatMessage) {
+ val message = chatRoom.createForwardMessage(chatMessage)
+ message?.send()
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageViewModel.kt
new file mode 100644
index 000000000..949f3dccf
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageViewModel.kt
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.os.CountDownTimer
+import android.text.Spanned
+import androidx.lifecycle.MutableLiveData
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.LinphoneApplication.Companion.corePreferences
+import org.linphone.R
+import org.linphone.compatibility.Compatibility
+import org.linphone.contact.GenericContactViewModel
+import org.linphone.core.ChatMessage
+import org.linphone.core.ChatMessageListenerStub
+import org.linphone.core.Content
+import org.linphone.core.tools.Log
+import org.linphone.mediastream.Version
+import org.linphone.utils.AppUtils
+import org.linphone.utils.PermissionHelper
+import org.linphone.utils.TimestampUtils
+
+class ChatMessageViewModel(
+ val chatMessage: ChatMessage,
+ private val contentListener: OnContentClickedListener? = null
+) : GenericContactViewModel(chatMessage.fromAddress) {
+ val sendInProgress = MutableLiveData()
+
+ val transferInProgress = 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: Spanned? by lazy {
+ if (chatMessage.textContent != null) AppUtils.getTextWithHttpLinks(chatMessage.textContent) else null
+ }
+
+ 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)
+
+ // TODO FIXME : find a way to refresh outgoing message downloaded
+ if (state == ChatMessage.State.FileTransferDone && !message.isOutgoing) {
+ Log.i("[Chat Message] File transfer done")
+ // No need to refresh content lists on outgoing messages after file transfer is done
+ // It will even cause the app to crash if updateContentsList is not call right after
+ updateContentsList()
+
+ if (!message.isEphemeral && corePreferences.makePublicDownloadedImages) {
+ if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStorage()) {
+ for (content in message.contents) {
+ if (content.isFile && content.filePath != null && content.userData == null) {
+ addContentToMediaStore(content)
+ }
+ }
+ } else {
+ Log.e("[Chat Message] Can't make file public, app doesn't have WRITE_EXTERNAL_STORAGE permission")
+ }
+ }
+ }
+ }
+
+ override fun onEphemeralMessageTimerStarted(message: ChatMessage) {
+ updateEphemeralTimer()
+ }
+ }
+
+ 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
+ time.value = TimestampUtils.toString(chatMessage.time)
+ updateEphemeralTimer()
+
+ updateChatMessageState(chatMessage.state)
+ updateContentsList()
+ }
+
+ override fun onCleared() {
+ chatMessage.removeListener(listener)
+
+ super.onCleared()
+ }
+
+ fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
+ 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
+ }
+ }
+ }
+
+ private fun updateChatMessageState(state: ChatMessage.State) {
+ transferInProgress.value = state == ChatMessage.State.FileTransferInProgress
+
+ sendInProgress.value = state == ChatMessage.State.InProgress || state == ChatMessage.State.FileTransferInProgress
+
+ showImdn.value = when (state) {
+ ChatMessage.State.DeliveredToUser, ChatMessage.State.Displayed, ChatMessage.State.NotDelivered -> true
+ else -> false
+ }
+
+ imdnIcon.value = when (state) {
+ ChatMessage.State.DeliveredToUser -> R.drawable.imdn_received
+ ChatMessage.State.Displayed -> R.drawable.imdn_read
+ else -> R.drawable.imdn_error
+ }
+ }
+
+ private fun updateContentsList() {
+ val list = arrayListOf()
+ for (content in chatMessage.contents) {
+ if (content.isFileTransfer || content.isFile) {
+ list.add(ChatMessageContentViewModel(content, chatMessage, contentListener))
+ }
+ }
+ contents.value = list
+ }
+
+ 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))
+ }
+ }
+
+ private fun addContentToMediaStore(content: Content) {
+ when (content.type) {
+ "image" -> {
+ if (Compatibility.addImageToMediaStore(coreContext.context, content)) {
+ Log.i("[Chat Message] Adding image ${content.name} terminated")
+ } else {
+ Log.e("[Chat Message] Something went wrong while copying file...")
+ }
+ }
+ "video" -> {
+ if (Compatibility.addVideoToMediaStore(coreContext.context, content)) {
+ Log.i("[Chat Message] Adding video ${content.name} terminated")
+ } else {
+ Log.e("[Chat Message] Something went wrong while copying file...")
+ }
+ }
+ "audio" -> {
+ if (Compatibility.addAudioToMediaStore(coreContext.context, content)) {
+ Log.i("[Chat Message] Adding audio ${content.name} terminated")
+ } else {
+ Log.e("[Chat Message] Something went wrong while copying file...")
+ }
+ }
+ else -> {
+ Log.w("[Chat Message] File ${content.name} isn't either an image, an audio file or a video, can't add it to the Media Store")
+ }
+ }
+ }
+}
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
new file mode 100644
index 000000000..0951ed904
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessagesListViewModel.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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 org.linphone.core.*
+import org.linphone.core.tools.Log
+import org.linphone.mediastream.Version
+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 chatMessageListener: ChatMessageListenerStub = object : ChatMessageListenerStub() {
+ override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
+ if (state == ChatMessage.State.Displayed) {
+ message.removeListener(this)
+ }
+
+ val position: Int? = message.userData as? Int?
+ if (position != null) {
+ messageUpdatedEvent.value = Event(position)
+ }
+ }
+ }
+
+ private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
+ override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) {
+ chatRoom.markAsRead()
+
+ val position = events.value?.size ?: 0
+
+ if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
+ val chatMessage = eventLog.chatMessage
+ chatMessage.userData = position
+ chatMessage.addListener(chatMessageListener)
+
+ if (Version.sdkStrictlyBelow(Version.API29_ANDROID_10) && !PermissionHelper.get().hasWriteExternalStorage()) {
+ 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)
+ }
+
+ override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) {
+ val position = events.value?.size ?: 0
+
+ if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
+ val chatMessage = eventLog.chatMessage
+ chatMessage.userData = position
+ chatMessage.addListener(chatMessageListener)
+ }
+
+ 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) {
+ addEvent(eventLog)
+ }
+
+ override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) {
+ addEvent(eventLog)
+ }
+
+ override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
+ Log.i("[Chat Messages] An ephemeral chat message has expired, removing it from event list")
+ deleteMessage(eventLog.chatMessage)
+ }
+
+ override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
+ addEvent(eventLog)
+ }
+ }
+
+ init {
+ chatRoom.addListener(chatRoomListener)
+
+ events.value = getEvents()
+ }
+
+ override fun onCleared() {
+ chatRoom.removeListener(chatRoomListener)
+
+ super.onCleared()
+ }
+
+ fun resendMessage(chatMessage: ChatMessage) {
+ val position: Int = chatMessage.userData as Int
+ chatMessage.resend()
+ messageUpdatedEvent.value = Event(position)
+ }
+
+ fun deleteMessage(chatMessage: ChatMessage) {
+ val position: Int = chatMessage.userData as Int
+ LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
+ chatRoom.deleteMessage(chatMessage)
+
+ val list = arrayListOf()
+ list.addAll(events.value.orEmpty())
+ list.removeAt(position)
+ events.value = list
+ }
+
+ fun deleteEventLogs(listToDelete: ArrayList) {
+ val list = arrayListOf()
+ list.addAll(events.value.orEmpty())
+
+ for (eventLog in listToDelete) {
+ LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
+ eventLog.deleteFromDatabase()
+ list.remove(eventLog)
+ }
+
+ events.value = list
+ }
+
+ 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 (message in history) {
+ list.add(message)
+ }
+ list.addAll(events.value.orEmpty())
+ events.value = list
+ }
+ }
+
+ private fun addEvent(eventLog: EventLog) {
+ val list = arrayListOf()
+ list.addAll(events.value.orEmpty())
+ list.add(eventLog)
+ events.value = list
+ }
+
+ private fun getEvents(): ArrayList {
+ val list = arrayListOf()
+ val history = chatRoom.getHistoryEvents(MESSAGES_PER_PAGE)
+ for (message in history) {
+ list.add(message)
+ }
+ return list
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationContactViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationContactViewModel.kt
new file mode 100644
index 000000000..1346f53e8
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationContactViewModel.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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 org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.contact.Contact
+import org.linphone.contact.ContactViewModelInterface
+import org.linphone.core.*
+import org.linphone.utils.LinphoneUtils
+
+class ChatRoomCreationContactViewModel(private val searchResult: SearchResult) : ViewModel(), ContactViewModelInterface {
+ override val contact = MutableLiveData()
+
+ override val displayName: String by lazy {
+ when {
+ searchResult.friend != null -> searchResult.friend.name
+ searchResult.address != null -> LinphoneUtils.getDisplayName(searchResult.address)
+ else -> searchResult.phoneNumber
+ }
+ }
+
+ 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 ?: searchResult.address.asStringUriOnly()
+ }
+
+ val address: Address by lazy {
+ searchResult.address
+ }
+
+ val hasLimeX3DHCapability: Boolean
+ get() = searchResult.hasCapability(FriendCapability.LimeX3Dh)
+
+ var listener: ChatRoomCreationContactSelectionListener? = null
+
+ init {
+ isDisabled.value = false
+ isSelected.value = false
+ searchMatchingContact()
+ }
+
+ private fun searchMatchingContact() {
+ if (searchResult.address != null) {
+ contact.value =
+ coreContext.contactsManager.findContactByAddress(searchResult.address)
+ } else if (searchResult.phoneNumber != null) {
+ contact.value = coreContext.contactsManager.findContactByPhoneNumber(searchResult.phoneNumber)
+ }
+ }
+}
+
+interface ChatRoomCreationContactSelectionListener {
+ fun onUnSelected(searchResult: SearchResult)
+}
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
new file mode 100644
index 000000000..ba7e77422
--- /dev/null
+++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatRoomCreationViewModel.kt
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2010-2020 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for 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.viewmodels.ErrorReportingViewModel
+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
+
+class ChatRoomCreationViewModel : ErrorReportingViewModel() {
+ val chatRoomCreatedEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ val createGroupChat = MutableLiveData()
+
+ val sipContactsSelected = MutableLiveData()
+
+ val isEncrypted = MutableLiveData()
+
+ val contactsList = MutableLiveData>()
+
+ val waitForChatRoomCreation = MutableLiveData()
+
+ val selectedAddresses = MutableLiveData>()
+
+ val limeAvailable: Boolean = LinphoneUtils.isLimeAvailable()
+
+ private var filter: String = ""
+
+ private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
+ override fun onContactsUpdated() {
+ Log.i("[Chat Room Creation] Contacts have changed")
+ updateContactsList()
+ }
+ }
+
+ 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
+ onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
+ }
+ }
+ }
+
+ init {
+ createGroupChat.value = false
+ sipContactsSelected.value = true
+ isEncrypted.value = false
+
+ selectedAddresses.value = arrayListOf()
+
+ updateContactsList()
+
+ coreContext.contactsManager.addListener(contactsUpdatedListener)
+ waitForChatRoomCreation.value = false
+ }
+
+ override fun onCleared() {
+ coreContext.contactsManager.removeListener(contactsUpdatedListener)
+
+ super.onCleared()
+ }
+
+ fun updateEncryption(encrypted: Boolean) {
+ isEncrypted.value = encrypted
+ }
+
+ fun filter(search: String) {
+ if (filter.isNotEmpty() && filter.length > search.length) {
+ coreContext.contactsManager.magicSearch.resetSearchCache()
+ }
+ filter = search
+
+ updateContactsList()
+ }
+
+ fun updateContactsList() {
+ val domain = if (sipContactsSelected.value == true) coreContext.core.defaultProxyConfig?.domain ?: "" else ""
+ val results = coreContext.contactsManager.magicSearch.getContactListFromFilter(filter, domain)
+
+ val list = arrayListOf()
+ for (result in results) {
+ list.add(result)
+ }
+ contactsList.value = list
+ }
+
+ fun toggleSelectionForSearchResult(searchResult: SearchResult) {
+ if (searchResult.address != null) {
+ toggleSelectionForAddress(searchResult.address)
+ }
+ }
+
+ fun toggleSelectionForAddress(address: Address) {
+ val list = arrayListOf()
+ list.addAll(selectedAddresses.value.orEmpty())
+
+ val found = list.find {
+ if (address != null) it.weakEqual(address) else false
+ }
+
+ if (found != null) {
+ list.remove(found)
+ } else {
+ val contact = coreContext.contactsManager.findContactByAddress(address)
+ if (contact != null) address.displayName = contact.fullName
+ list.add(address)
+ }
+
+ selectedAddresses.value = list
+ }
+
+ fun createOneToOneChat(searchResult: SearchResult) {
+ waitForChatRoomCreation.value = true
+ val defaultProxyConfig = coreContext.core.defaultProxyConfig
+ var room: ChatRoom?
+
+ if (defaultProxyConfig == null) {
+ val address = searchResult.address ?: coreContext.core.interpretUrl(searchResult.phoneNumber)
+ if (address == null) {
+ Log.e("[Chat Room Creation] Can't get a valid address from search result $searchResult")
+ onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
+ waitForChatRoomCreation.value = false
+ return
+ }
+
+ Log.w("[Chat Room Creation] No default proxy config found, creating basic chat room without local identity with ${address.asStringUriOnly()}")
+ room = coreContext.core.getChatRoom(address)
+ if (room != null) {
+ chatRoomCreatedEvent.value = Event(room)
+ } else {
+ Log.e("[Chat Room Creation] Couldn't create chat room with remote ${address.asStringUriOnly()}")
+ }
+ waitForChatRoomCreation.value = false
+ return
+ }
+
+ val encrypted = isEncrypted.value == true
+ room = coreContext.core.findOneToOneChatRoom(defaultProxyConfig.identityAddress, searchResult.address, encrypted)
+ if (room == null) {
+ Log.w("[Chat Room Creation] Couldn't find existing 1-1 chat room with remote ${searchResult.address.asStringUriOnly()}, encryption=$encrypted and local identity ${defaultProxyConfig.identityAddress.asStringUriOnly()}")
+ if (encrypted) {
+ val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
+ // This will set the backend to FlexisipChat automatically
+ params.enableEncryption(true)
+ params.enableGroup(false)
+
+ val participants = arrayOfNulls