Kotlin rewrite
This commit is contained in:
parent
1198551254
commit
5f1984fe4b
727 changed files with 35466 additions and 59669 deletions
136
app/build.gradle
136
app/build.gradle
|
@ -1,5 +1,13 @@
|
||||||
apply plugin: 'com.android.application'
|
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() {
|
static def getPackageName() {
|
||||||
return "org.linphone"
|
return "org.linphone"
|
||||||
}
|
}
|
||||||
|
@ -10,7 +18,7 @@ static def firebaseEnabled() {
|
||||||
}
|
}
|
||||||
|
|
||||||
task getGitVersion() {
|
task getGitVersion() {
|
||||||
def gitVersion = "4.4.0"
|
def gitVersion = "5.0"
|
||||||
def gitVersionStream = new ByteArrayOutputStream()
|
def gitVersionStream = new ByteArrayOutputStream()
|
||||||
def gitCommitsCount = new ByteArrayOutputStream()
|
def gitCommitsCount = new ByteArrayOutputStream()
|
||||||
def gitCommitHash = new ByteArrayOutputStream()
|
def gitCommitHash = new ByteArrayOutputStream()
|
||||||
|
@ -41,69 +49,18 @@ task getGitVersion() {
|
||||||
project.version = gitVersion
|
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 'getGitVersion'
|
||||||
project.tasks['preBuild'].dependsOn 'linphoneSdkSource'
|
project.tasks['preBuild'].dependsOn 'linphoneSdkSource'
|
||||||
|
|
||||||
android {
|
android {
|
||||||
lintOptions {
|
|
||||||
abortOnError false
|
|
||||||
}
|
|
||||||
|
|
||||||
compileSdkVersion 29
|
compileSdkVersion 29
|
||||||
|
buildToolsVersion "29.0.2"
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdkVersion 23
|
minSdkVersion 23
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode 4400
|
versionCode 4300
|
||||||
versionName "${project.version}"
|
versionName "${project.version}"
|
||||||
applicationId getPackageName()
|
applicationId getPackageName()
|
||||||
multiDexEnabled true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applicationVariants.all { variant ->
|
applicationVariants.all { variant ->
|
||||||
|
@ -114,12 +71,10 @@ android {
|
||||||
// https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for extractNativeLibs
|
// https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for extractNativeLibs
|
||||||
if (variant.buildType.name == "release") {
|
if (variant.buildType.name == "release") {
|
||||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
||||||
linphone_file_provider: getPackageName() + ".provider",
|
linphone_file_provider: getPackageName() + ".fileprovider"]
|
||||||
extractNativeLibs: "false"]
|
|
||||||
} else {
|
} else {
|
||||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
||||||
linphone_file_provider: getPackageName() + ".debug.provider",
|
linphone_file_provider: getPackageName() + ".debug.fileprovider"]
|
||||||
extractNativeLibs: "true"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,20 +98,21 @@ android {
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
|
||||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
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"
|
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
||||||
|
|
||||||
if (!firebaseEnabled()) {
|
if (!firebaseEnabled()) {
|
||||||
resValue "string", "gcm_defaultSenderId", "none"
|
resValue "string", "gcm_defaultSenderId", "none"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug {
|
debug {
|
||||||
applicationIdSuffix ".debug"
|
applicationIdSuffix ".debug"
|
||||||
debuggable true
|
debuggable true
|
||||||
jniDebuggable true
|
jniDebuggable true
|
||||||
|
|
||||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
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"
|
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
||||||
|
|
||||||
if (!firebaseEnabled()) {
|
if (!firebaseEnabled()) {
|
||||||
|
@ -165,35 +121,44 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceSets {
|
dataBinding {
|
||||||
main {
|
enabled = true
|
||||||
java.excludes = excludeFiles
|
}
|
||||||
|
}
|
||||||
|
|
||||||
packagingOptions {
|
repositories {
|
||||||
excludes = excludePackage
|
maven {
|
||||||
}
|
url file(LinphoneSdkBuildDir + '/maven_repository/')
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
packagingOptions {
|
/*maven {
|
||||||
pickFirst 'META-INF/NOTICE'
|
url "https://linphone.org/maven_repository"
|
||||||
pickFirst 'META-INF/LICENSE'
|
}*/
|
||||||
exclude 'META-INF/MANIFEST.MF'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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()) {
|
if (firebaseEnabled()) {
|
||||||
implementation 'com.google.firebase:firebase-messaging:19.0.1'
|
implementation 'com.google.firebase:firebase-messaging:19.0.1'
|
||||||
}
|
}
|
||||||
implementation 'androidx.media:media:1.2.0'
|
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
implementation 'org.linphone:linphone-sdk-android:4.4+'
|
||||||
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+"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (firebaseEnabled()) {
|
if (firebaseEnabled()) {
|
||||||
apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
}
|
}
|
||||||
|
@ -210,12 +175,9 @@ task generateContactsXml(type: Copy) {
|
||||||
}
|
}
|
||||||
project.tasks['preBuild'].dependsOn 'generateContactsXml'
|
project.tasks['preBuild'].dependsOn 'generateContactsXml'
|
||||||
|
|
||||||
apply plugin: "com.diffplug.gradle.spotless"
|
ktlint {
|
||||||
spotless {
|
android = true
|
||||||
java {
|
ignoreFailures = true
|
||||||
target '**/*.java'
|
|
||||||
googleJavaFormat('1.6').aosp()
|
|
||||||
removeUnusedImports()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
project.tasks['preBuild'].dependsOn 'spotlessApply'
|
|
||||||
|
project.tasks['preBuild'].dependsOn 'ktlintFormat'
|
|
@ -4,7 +4,7 @@
|
||||||
<ContactsDataKind
|
<ContactsDataKind
|
||||||
android:detailColumn="data3"
|
android:detailColumn="data3"
|
||||||
android:detailSocialSummary="true"
|
android:detailSocialSummary="true"
|
||||||
android:icon="@drawable/linphone_logo"
|
android:icon="@drawable/linphone_logo_tinted"
|
||||||
android:mimeType="vnd.android.cursor.item/vnd.%%PACKAGE_NAME%%.provider.sip_address"
|
android:mimeType="vnd.android.cursor.item/vnd.%%PACKAGE_NAME%%.provider.sip_address"
|
||||||
android:summaryColumn="data2" />
|
android:summaryColumn="data2" />
|
||||||
<!-- You can't use @string/linphone_address_mime_type above ! You have to hardcode it... -->
|
<!-- You can't use @string/linphone_address_mime_type above ! You have to hardcode it... -->
|
||||||
|
|
22
app/proguard-rules.pro
vendored
22
app/proguard-rules.pro
vendored
|
@ -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
|
||||||
|
|
247
app/src/main/AndroidManifest.xml
Executable file → Normal file
247
app/src/main/AndroidManifest.xml
Executable file → Normal file
|
@ -1,222 +1,122 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="org.linphone"
|
package="org.linphone">
|
||||||
android:installLocation="auto">
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||||
<!-- Needed for bluetooth headset -->
|
|
||||||
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.VIBRATE" />
|
|
||||||
<uses-permission android:name="android.permission.CAMERA" />
|
|
||||||
<!-- Needed to allow Linphone to install on tablets, since android.permission.CAMERA implies android.hardware.camera and android.hardware.camera.autofocus are required -->
|
|
||||||
<uses-feature
|
|
||||||
android:name="android.hardware.camera"
|
|
||||||
android:required="false" />
|
|
||||||
|
|
||||||
<!-- Needed to be able to detect a GSM call and thus pause any active SIP call, and auto fill the phone number field in assistant -->
|
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
|
||||||
<!-- Needed to be able to pick images from SD card to share in chat message -->
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<!-- Needed to store received images if the user wants to -->
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
||||||
<!-- Needed to use our own Contact editor -->
|
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||||
<!-- Needed to route the audio to the bluetooth headset if available -->
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
|
||||||
<!-- Needed to pre fill the wizard email field (only if enabled in custom settings) -->
|
|
||||||
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
|
||||||
<!-- Needed by the SDK to be able to use WifiManager.MulticastLock -->
|
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
|
|
||||||
<!-- Required for contacts sync account -->
|
|
||||||
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
|
||||||
<!-- Required if Android < 6.0 to be able to use AccountManager for contacts & email auto-fill in assistant -->
|
|
||||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
|
||||||
<!-- Needed for overlay widget and floating notifications -->
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
|
||||||
<!-- Needed for kill application yourself -->
|
|
||||||
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
|
|
||||||
<!-- Needed for auto start at boot and to ensure the service won't be killed by OS while in call -->
|
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
||||||
<!-- Needed to get the current Do Not Disturb policy -->
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
|
|
||||||
<!-- Needed for full screen intent in notifications -->
|
|
||||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
|
||||||
|
|
||||||
<supports-screens
|
<!-- Helps filling phone number and country code in assistant -->
|
||||||
android:anyDensity="true"
|
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||||
android:largeScreens="true"
|
|
||||||
android:normalScreens="true"
|
<!-- Needed for auto start at boot and to ensure the service won't be killed by OS while in call -->
|
||||||
android:smallScreens="true"
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
android:xlargeScreens="true" />
|
|
||||||
|
<!-- Needed for full screen intent in incoming call notifications -->
|
||||||
|
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||||
|
<!-- To vibrate while incoming call -->
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<!-- Needed to shared downloaded files if setting is on -->
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="28" />
|
||||||
|
|
||||||
|
<!-- Both permissions below are for contacts sync account -->
|
||||||
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||||
|
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
|
||||||
|
|
||||||
|
<!-- Needed for overlay -->
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".LinphoneApplication"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:resizeableActivity="true"
|
android:theme="@style/AppTheme">
|
||||||
android:theme="@style/LinphoneStyle"
|
|
||||||
android:extractNativeLibs="${extractNativeLibs}"
|
|
||||||
android:requestLegacyExternalStorage="true">
|
|
||||||
|
|
||||||
<!-- Starting activities -->
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.LinphoneLauncherActivity"
|
android:name=".activities.launcher.LauncherActivity"
|
||||||
android:noHistory="true">
|
android:noHistory="true"
|
||||||
|
android:theme="@style/LauncherTheme">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Main activities -->
|
<activity android:name=".activities.main.MainActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<nav-graph android:value="@navigation/main_nav_graph" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".dialer.DialerActivity"
|
|
||||||
android:launchMode="singleTop">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.DIAL" />
|
|
||||||
<action android:name="android.intent.action.CALL" />
|
|
||||||
<action android:name="android.intent.action.CALL_PRIVILEGED" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="tel" />
|
|
||||||
<data android:scheme="sip" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.SENDTO" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
<data android:scheme="sip" />
|
|
||||||
<data android:scheme="imto" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="org.linphone.intent.action.CallLaunched" />
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
|
||||||
</intent-filter>
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".activities.AboutActivity"
|
|
||||||
android:noHistory="true"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".recording.RecordingsActivity"
|
|
||||||
android:noHistory="true"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".settings.SettingsActivity"/>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".chat.ChatActivity"
|
|
||||||
android:launchMode="singleTop">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="android.intent.action.MAIN" />
|
|
||||||
</intent-filter>
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.OPENABLE" />
|
|
||||||
|
|
||||||
<data android:mimeType="text/*" />
|
<data android:mimeType="text/*" />
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
<data android:mimeType="audio/*" />
|
<data android:mimeType="audio/*" />
|
||||||
<data android:mimeType="video/*" />
|
<data android:mimeType="video/*" />
|
||||||
<data android:mimeType="application/*" />
|
<data android:mimeType="application/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<category android:name="android.intent.category.OPENABLE" />
|
|
||||||
|
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".contacts.ContactsActivity"
|
|
||||||
android:launchMode="singleTop">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
<category android:name="android.intent.category.BROWSABLE" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
<data android:mimeType="${linphone_address_mime_type}" />
|
<data android:mimeType="${linphone_address_mime_type}" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".history.HistoryActivity"
|
|
||||||
android:launchMode="singleTop">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.DIAL" />
|
||||||
|
<action android:name="android.intent.action.CALL" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<data android:scheme="tel" />
|
||||||
|
<data android:scheme="sip" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- Call activities -->
|
<activity android:name=".activities.assistant.AssistantActivity"
|
||||||
|
android:windowSoftInputMode="adjustResize"/>
|
||||||
|
|
||||||
<activity
|
<activity android:name=".activities.call.CallActivity"
|
||||||
android:name=".call.CallIncomingActivity"
|
android:launchMode="singleTop"
|
||||||
|
android:showWhenLocked="true"
|
||||||
|
android:supportsPictureInPicture="true" />
|
||||||
|
|
||||||
|
<activity android:name=".activities.call.IncomingCallActivity"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:noHistory="true"
|
android:noHistory="true"
|
||||||
android:showWhenLocked="true"
|
android:showWhenLocked="true"
|
||||||
android:turnScreenOn="true"/>
|
android:turnScreenOn="true" />
|
||||||
|
|
||||||
<activity
|
<activity android:name=".activities.call.OutgoingCallActivity"
|
||||||
android:name=".call.CallOutgoingActivity"
|
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:noHistory="true"/>
|
android:noHistory="true" />
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".call.CallActivity"
|
|
||||||
android:launchMode="singleTop"
|
|
||||||
android:showWhenLocked="true"
|
|
||||||
android:supportsPictureInPicture="true"/>
|
|
||||||
|
|
||||||
<!-- Assistant activities -->
|
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.MenuAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.AccountConnectionAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.EmailAccountCreationAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.EmailAccountValidationAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.PhoneAccountCreationAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.PhoneAccountValidationAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.PhoneAccountLinkingAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.GenericConnectionAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.QrCodeConfigurationAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.RemoteConfigurationAssistantActivity"/>
|
|
||||||
<activity
|
|
||||||
android:name=".assistant.EchoCancellerCalibrationAssistantActivity"/>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Services -->
|
<!-- Services -->
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".service.LinphoneService"
|
android:name=".core.CoreService"
|
||||||
android:label="@string/service_name" />
|
android:foregroundServiceType="phoneCall"
|
||||||
|
android:stopWithTask="false"
|
||||||
|
android:label="@string/app_name" />
|
||||||
|
|
||||||
|
<service android:name="org.linphone.core.tools.firebase.FirebaseMessaging"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".sync.SyncService"
|
android:name=".contact.DummySyncService"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.SyncAdapter" />
|
<action android:name="android.content.SyncAdapter" />
|
||||||
|
@ -224,13 +124,13 @@
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
android:resource="@xml/syncadapter" />
|
android:resource="@xml/sync_adapter" />
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||||
android:resource="@xml/contacts" />
|
android:resource="@xml/contacts" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name=".sync.AuthenticationService">
|
<service android:name=".contact.DummyAuthenticationService">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.accounts.AccountAuthenticator" />
|
<action android:name="android.accounts.AccountAuthenticator" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -240,21 +140,11 @@
|
||||||
android:resource="@xml/authenticator" />
|
android:resource="@xml/authenticator" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name=".firebase.FirebaseMessaging"
|
|
||||||
android:enabled="true"
|
|
||||||
android:exported="true">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.google.firebase.MESSAGING_EVENT" />
|
|
||||||
</intent-filter>
|
|
||||||
</service>
|
|
||||||
|
|
||||||
<!-- Receivers -->
|
<!-- Receivers -->
|
||||||
|
|
||||||
<receiver android:name=".receivers.BootReceiver">
|
<receiver android:name=".core.CorePushReceiver">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
<action android:name="org.linphone.core.action.PUSH_RECEIVED"/>
|
||||||
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
|
||||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
@ -263,10 +153,10 @@
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<receiver
|
<receiver android:name=".core.BootReceiver">
|
||||||
android:name=".receivers.AccountEnableReceiver">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="org.linphone.intent.ACCOUNTACTIVATE" />
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||||
|
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
@ -283,4 +173,5 @@
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
|
||||||
|
</manifest>
|
|
@ -21,12 +21,6 @@
|
||||||
<entry name="stun_server" overwrite="true"></entry>
|
<entry name="stun_server" overwrite="true"></entry>
|
||||||
<entry name="protocols" overwrite="true"></entry>
|
<entry name="protocols" overwrite="true"></entry>
|
||||||
</section>
|
</section>
|
||||||
<section name="sip">
|
|
||||||
<entry name="rls_uri" overwrite="true"></entry>
|
|
||||||
</section>
|
|
||||||
<section name="lime">
|
|
||||||
<entry name="x3dh_server_url" overwrite="true"></entry>
|
|
||||||
</section>
|
|
||||||
<section name="assistant">
|
<section name="assistant">
|
||||||
<entry name="domain" overwrite="true"></entry>
|
<entry name="domain" overwrite="true"></entry>
|
||||||
<entry name="algorithm" overwrite="true">MD5</entry>
|
<entry name="algorithm" overwrite="true">MD5</entry>
|
|
@ -1,3 +1,6 @@
|
||||||
|
|
||||||
|
## Start of default rc
|
||||||
|
|
||||||
[sip]
|
[sip]
|
||||||
contact="Linphone Android" <sip:linphone.android@unknown-host>
|
contact="Linphone Android" <sip:linphone.android@unknown-host>
|
||||||
use_info=0
|
use_info=0
|
||||||
|
@ -34,4 +37,6 @@ history_max_size=100
|
||||||
|
|
||||||
[in-app-purchase]
|
[in-app-purchase]
|
||||||
server_url=https://subscribe.linphone.org:444/inapp.php
|
server_url=https://subscribe.linphone.org:444/inapp.php
|
||||||
purchasable_items_ids=test_account_subscription
|
purchasable_items_ids=test_account_subscription
|
||||||
|
|
||||||
|
## End of default rc
|
|
@ -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.
|
#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.
|
#Paths to resources must be set from LinphoneManager, after creating LinphoneCore.
|
||||||
|
|
||||||
[net]
|
[net]
|
||||||
mtu=1300
|
mtu=1300
|
||||||
force_ice_disablement=0
|
force_ice_disablement=0
|
||||||
|
@ -36,4 +38,6 @@ prefer_basic_chat_room=1
|
||||||
xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
|
xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
|
||||||
|
|
||||||
[lime]
|
[lime]
|
||||||
lime_update_threshold=-1
|
lime_update_threshold=-1
|
||||||
|
|
||||||
|
## End of factory rc
|
|
@ -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);
|
|
||||||
}
|
|
55
app/src/main/java/org/linphone/LinphoneApplication.kt
Normal file
55
app/src/main/java/org/linphone/LinphoneApplication.kt
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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<CoreStartedListener> 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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
83
app/src/main/java/org/linphone/activities/GenericActivity.kt
Normal file
83
app/src/main/java/org/linphone/activities/GenericActivity.kt
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.pm.ActivityInfo
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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<String> 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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
* This file is part of linphone-android
|
||||||
* (see https://www.linphone.org).
|
* (see https://www.linphone.org).
|
||||||
|
@ -17,8 +17,8 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.linphone.call.views;
|
package org.linphone.activities
|
||||||
|
|
||||||
public interface CallIncomingButtonListener {
|
interface SnackBarActivity {
|
||||||
void onAction();
|
fun showSnackBar(resourceId: Int)
|
||||||
}
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.BaseAdapter
|
||||||
|
import android.widget.Filter
|
||||||
|
import android.widget.Filterable
|
||||||
|
import android.widget.TextView
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.core.DialPlan
|
||||||
|
import org.linphone.core.Factory
|
||||||
|
|
||||||
|
class CountryPickerAdapter : BaseAdapter(), Filterable {
|
||||||
|
private var countries: ArrayList<DialPlan>
|
||||||
|
|
||||||
|
init {
|
||||||
|
val dialPlans = Factory.instance().dialPlans
|
||||||
|
countries = arrayListOf()
|
||||||
|
countries.addAll(dialPlans)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||||
|
val view: View = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.assistant_country_picker_cell, parent, false)
|
||||||
|
val dialPlan: DialPlan = countries[position]
|
||||||
|
|
||||||
|
val name = view.findViewById<TextView>(R.id.country_name)
|
||||||
|
name.text = dialPlan.country
|
||||||
|
|
||||||
|
val dialCode = view.findViewById<TextView>(R.id.country_prefix)
|
||||||
|
dialCode.text = String.format("(%s)", dialPlan.countryCallingCode)
|
||||||
|
|
||||||
|
view.tag = dialPlan
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItem(position: Int): DialPlan {
|
||||||
|
return countries[position]
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return position.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCount(): Int {
|
||||||
|
return countries.size
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilter(): Filter {
|
||||||
|
return object : Filter() {
|
||||||
|
override fun performFiltering(constraint: CharSequence): FilterResults {
|
||||||
|
val filteredCountries = arrayListOf<DialPlan>()
|
||||||
|
for (dialPlan in Factory.instance().dialPlans) {
|
||||||
|
if (dialPlan.country.contains(constraint, ignoreCase = true) ||
|
||||||
|
dialPlan.countryCallingCode.contains(constraint)
|
||||||
|
) {
|
||||||
|
filteredCountries.add(dialPlan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val filterResults = FilterResults()
|
||||||
|
filterResults.values = filteredCountries
|
||||||
|
return filterResults
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun publishResults(
|
||||||
|
constraint: CharSequence,
|
||||||
|
results: FilterResults
|
||||||
|
) {
|
||||||
|
countries = results.values as ArrayList<DialPlan>
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<out String>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<out String>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.fragments
|
||||||
|
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context.CLIPBOARD_SERVICE
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<out String>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<String>()
|
||||||
|
|
||||||
|
val phoneNumber = MutableLiveData<String>()
|
||||||
|
val phoneNumberError = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val countryName: LiveData<String> = 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<String> {
|
||||||
|
val country = MutableLiveData<String>()
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.*
|
||||||
|
import org.linphone.core.AccountCreator
|
||||||
|
import org.linphone.core.AccountCreatorListenerStub
|
||||||
|
import org.linphone.core.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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return AccountLoginViewModel(accountCreator) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||||
|
val loginWithUsernamePassword = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val username = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val password = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||||
|
|
||||||
|
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val leaveAssistantEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.core.Core
|
||||||
|
import org.linphone.core.CoreListenerStub
|
||||||
|
import org.linphone.core.EcCalibratorStatus
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
|
class EchoCancellerCalibrationViewModel : ViewModel() {
|
||||||
|
val echoCalibrationTerminated = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
private val listener = object : CoreListenerStub() {
|
||||||
|
override fun onEcCalibrationResult(core: Core, status: EcCalibratorStatus, delayMs: Int) {
|
||||||
|
if (status == EcCalibratorStatus.InProgress) return
|
||||||
|
echoCancellerCalibrationFinished(status, delayMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
coreContext.core.addListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startEchoCancellerCalibration() {
|
||||||
|
coreContext.core.startEchoCancellerCalibration()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun echoCancellerCalibrationFinished(status: EcCalibratorStatus, delay: Int) {
|
||||||
|
coreContext.core.removeListener(listener)
|
||||||
|
when (status) {
|
||||||
|
EcCalibratorStatus.DoneNoEcho -> {
|
||||||
|
Log.i("[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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.core.AccountCreator
|
||||||
|
import org.linphone.core.AccountCreatorListenerStub
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.AppUtils
|
||||||
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
|
class EmailAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||||
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return EmailAccountCreationViewModel(accountCreator) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailAccountCreationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||||
|
val username = MutableLiveData<String>()
|
||||||
|
val usernameError = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val email = MutableLiveData<String>()
|
||||||
|
val emailError = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val password = MutableLiveData<String>()
|
||||||
|
val passwordError = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val passwordConfirmation = MutableLiveData<String>()
|
||||||
|
val passwordConfirmationError = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||||
|
|
||||||
|
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val goToEmailValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.core.AccountCreator
|
||||||
|
import org.linphone.core.AccountCreatorListenerStub
|
||||||
|
import org.linphone.core.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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return EmailAccountValidationViewModel(accountCreator) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||||
|
val email = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return GenericLoginViewModel(accountCreator) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewModel() {
|
||||||
|
val username = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val password = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val domain = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val displayName = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val transport = MutableLiveData<TransportType>()
|
||||||
|
|
||||||
|
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||||
|
|
||||||
|
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MediatorLiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.core.AccountCreator
|
||||||
|
import org.linphone.core.AccountCreatorListenerStub
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.AppUtils
|
||||||
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
|
class PhoneAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||||
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return PhoneAccountCreationViewModel(accountCreator) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||||
|
val username = MutableLiveData<String>()
|
||||||
|
val useUsername = MutableLiveData<Boolean>()
|
||||||
|
val usernameError = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||||
|
|
||||||
|
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.*
|
||||||
|
import org.linphone.core.AccountCreator
|
||||||
|
import org.linphone.core.AccountCreatorListenerStub
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
|
class PhoneAccountLinkingViewModelFactory(private val accountCreator: AccountCreator) :
|
||||||
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return PhoneAccountLinkingViewModel(accountCreator) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||||
|
val username = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val allowSkip = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val linkEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||||
|
|
||||||
|
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
val goToSmsValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.core.AccountCreator
|
||||||
|
import org.linphone.core.AccountCreatorListenerStub
|
||||||
|
import org.linphone.core.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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return PhoneAccountValidationViewModel(accountCreator) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PhoneAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||||
|
val phoneNumber = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val code = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val isLogin = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isCreation = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isLinking = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
val 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.core.Core
|
||||||
|
import org.linphone.core.CoreListenerStub
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
|
class QrCodeViewModel : ViewModel() {
|
||||||
|
val qrCodeFoundEvent = MutableLiveData<Event<String>>()
|
||||||
|
|
||||||
|
val showSwitchCamera = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
private val listener = object : CoreListenerStub() {
|
||||||
|
override fun onQrcodeFound(core: Core, result: String) {
|
||||||
|
Log.i("[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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.core.ConfiguringState
|
||||||
|
import org.linphone.core.Core
|
||||||
|
import org.linphone.core.CoreListenerStub
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
|
class RemoteProvisioningViewModel : ViewModel() {
|
||||||
|
val urlToFetch = MutableLiveData<String>()
|
||||||
|
val fetchInProgress = MutableLiveData<Boolean>()
|
||||||
|
val fetchSuccessfulEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
private val listener = object : CoreListenerStub() {
|
||||||
|
override fun onConfiguringStatus(core: Core, status: ConfiguringState, message: String?) {
|
||||||
|
fetchInProgress.value = false
|
||||||
|
when (status) {
|
||||||
|
ConfiguringState.Successful -> {
|
||||||
|
fetchSuccessfulEvent.value = Event(true)
|
||||||
|
}
|
||||||
|
ConfiguringState.Failed -> {
|
||||||
|
fetchSuccessfulEvent.value = Event(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.assistant.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import java.util.*
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||||
|
import org.linphone.core.*
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
|
||||||
|
class SharedAssistantViewModel : ViewModel() {
|
||||||
|
val remoteProvisioningUrl = MutableLiveData<String>()
|
||||||
|
|
||||||
|
private var accountCreator: AccountCreator
|
||||||
|
private var useGenericSipAccount: Boolean = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
Log.i("[Assistant] Loading linphone default values")
|
||||||
|
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||||
|
accountCreator = coreContext.core.createAccountCreator(corePreferences.xmlRpcServerUrl)
|
||||||
|
accountCreator.language = Locale.getDefault().language
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAccountCreator(genericAccountCreator: Boolean = false): AccountCreator {
|
||||||
|
if (genericAccountCreator != useGenericSipAccount) {
|
||||||
|
accountCreator.reset()
|
||||||
|
accountCreator.language = Locale.getDefault().language
|
||||||
|
|
||||||
|
if (genericAccountCreator) {
|
||||||
|
Log.i("[Assistant] Loading default values")
|
||||||
|
coreContext.core.loadConfigFromXml(corePreferences.defaultValuesPath)
|
||||||
|
} else {
|
||||||
|
Log.i("[Assistant] Loading linphone default values")
|
||||||
|
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||||
|
}
|
||||||
|
useGenericSipAccount = genericAccountCreator
|
||||||
|
}
|
||||||
|
return accountCreator
|
||||||
|
}
|
||||||
|
}
|
205
app/src/main/java/org/linphone/activities/call/CallActivity.kt
Normal file
205
app/src/main/java/org/linphone/activities/call/CallActivity.kt
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<String>()
|
||||||
|
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<String>(permissionsRequiredList.size)
|
||||||
|
permissionsRequiredList.toArray(permissionsRequired)
|
||||||
|
requestPermissions(permissionsRequired, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<String>()
|
||||||
|
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<String>(permissionsRequiredList.size)
|
||||||
|
permissionsRequiredList.toArray(permissionsRequired)
|
||||||
|
requestPermissions(permissionsRequired, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<ArrayList<StatItemViewModel>>()
|
||||||
|
|
||||||
|
val videoStats = MutableLiveData<ArrayList<StatItemViewModel>>()
|
||||||
|
|
||||||
|
val isVideoEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isExpanded = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
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<StatItemViewModel>()
|
||||||
|
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<StatItemViewModel>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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 <T : ViewModel?> create(modelClass: Class<T>): 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<Boolean>()
|
||||||
|
|
||||||
|
val callEndedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<CallViewModel>()
|
||||||
|
|
||||||
|
val callPausedByRemote = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val pausedCalls = MutableLiveData<ArrayList<CallViewModel>>()
|
||||||
|
|
||||||
|
val conferenceCalls = MutableLiveData<ArrayList<CallViewModel>>()
|
||||||
|
|
||||||
|
val isConferencePaused = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val noMoreCallEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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<CallViewModel>()
|
||||||
|
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<CallViewModel>()
|
||||||
|
list.addAll(pausedCalls.value.orEmpty())
|
||||||
|
|
||||||
|
val viewModel = CallViewModel(call)
|
||||||
|
list.add(viewModel)
|
||||||
|
pausedCalls.value = list
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun removeCallFromPausedListIfPresent(call: Call) {
|
||||||
|
val list = arrayListOf<CallViewModel>()
|
||||||
|
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<CallViewModel>()
|
||||||
|
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<CallViewModel>()
|
||||||
|
list.addAll(conferenceCalls.value.orEmpty())
|
||||||
|
|
||||||
|
for (viewModel in list) {
|
||||||
|
if (viewModel.call == call) {
|
||||||
|
list.remove(viewModel)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conferenceCalls.value = list
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Boolean>()
|
||||||
|
|
||||||
|
val videoEnabledEvent = MutableLiveData<Event<Boolean>>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Boolean>()
|
||||||
|
|
||||||
|
val isMuteMicrophoneEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isSpeakerSelected = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isBluetoothHeadsetSelected = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isVideoAvailable = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isVideoEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isVideoUpdateInProgress = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isPauseEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isRecording = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isConferencingAvailable = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val unreadMessagesCount = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
val numpadVisibility = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val optionsVisibility = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val audioRoutesVisibility = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val audioRoutesEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val chatClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val addCallClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val transferCallClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return IncomingCallViewModel(call) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IncomingCallViewModel(call: Call) : CallViewModel(call) {
|
||||||
|
val screenLocked = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val earlyMediaVideoEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val inviteWithVideo = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
* This file is part of linphone-android
|
||||||
* (see https://www.linphone.org).
|
* (see https://www.linphone.org).
|
||||||
|
@ -17,8 +17,12 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.linphone.compatibility;
|
package org.linphone.activities.call.viewmodels
|
||||||
|
|
||||||
public interface CompatibilityScaleGestureListener {
|
import androidx.lifecycle.MutableLiveData
|
||||||
boolean onScale(CompatibilityScaleGestureDetector detector);
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.linphone.utils.Event
|
||||||
|
|
||||||
|
class SharedCallViewModel : ViewModel() {
|
||||||
|
val toggleDrawerEvent = MutableLiveData<Event<Boolean>>()
|
||||||
}
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<String>()
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<ArrayList<CallStatisticsViewModel>>()
|
||||||
|
|
||||||
|
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<CallStatisticsViewModel>()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Int>()
|
||||||
|
val callQualityContentDescription = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
val encryptionIcon = MutableLiveData<Int>()
|
||||||
|
val encryptionContentDescription = MutableLiveData<Int>()
|
||||||
|
val encryptionIconVisible = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val showZrtpDialogEvent: MutableLiveData<Event<Call>> by lazy {
|
||||||
|
MutableLiveData<Event<Call>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
215
app/src/main/java/org/linphone/activities/main/MainActivity.kt
Normal file
215
app/src/main/java/org/linphone/activities/main/MainActivity.kt
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||||
|
val list = arrayListOf<String>()
|
||||||
|
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<Parcelable>(Intent.EXTRA_STREAM)?.let {
|
||||||
|
val list = arrayListOf<String>()
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.about
|
||||||
|
|
||||||
|
import android.content.*
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Boolean>()
|
||||||
|
|
||||||
|
val uploadFinishedEvent: MutableLiveData<Event<String>> by lazy {
|
||||||
|
MutableLiveData<Event<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
internal abstract class ChatScrollListener(private val mLayoutManager: LinearLayoutManager) :
|
||||||
|
RecyclerView.OnScrollListener() {
|
||||||
|
// The total number of items in the data set after the last load
|
||||||
|
private var previousTotalItemCount = 0
|
||||||
|
// True if we are still waiting for the last set of data to load.
|
||||||
|
private var loading = true
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
)
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.adapters
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.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<EventLog, LifecycleViewHolder>(ChatMessageDiffCallback()) {
|
||||||
|
companion object {
|
||||||
|
const val MAX_TIME_TO_GROUP_MESSAGES = 300 // 5 minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
val resendMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||||
|
MutableLiveData<Event<ChatMessage>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val deleteMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||||
|
MutableLiveData<Event<ChatMessage>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val forwardMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||||
|
MutableLiveData<Event<ChatMessage>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val showImdnForMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||||
|
MutableLiveData<Event<ChatMessage>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val addSipUriToContactEvent: MutableLiveData<Event<String>> by lazy {
|
||||||
|
MutableLiveData<Event<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val openContentEvent: MutableLiveData<Event<String>> by lazy {
|
||||||
|
MutableLiveData<Event<String>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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<EventLog>() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.lifecycle.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<SearchResult, ChatRoomCreationContactsAdapter.ViewHolder>(SearchResultDiffCallback()) {
|
||||||
|
val selectedContact = MutableLiveData<Event<SearchResult>>()
|
||||||
|
|
||||||
|
val selectedAddresses = MutableLiveData<ArrayList<Address>>()
|
||||||
|
|
||||||
|
var groupChatEnabled: Boolean = false
|
||||||
|
|
||||||
|
val securityEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
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<SearchResult>() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.lifecycle.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<ChatRoom, ChatRoomsListAdapter.ViewHolder>(ChatRoomDiffCallback()) {
|
||||||
|
val selectedChatRoomEvent: MutableLiveData<Event<ChatRoom>> by lazy {
|
||||||
|
MutableLiveData<Event<ChatRoom>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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<ChatRoom>() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.adapters
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.lifecycle.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<GroupChatRoomMember,
|
||||||
|
GroupInfoParticipantsAdapter.ViewHolder>(ParticipantDiffCallback()) {
|
||||||
|
private var showAdmin: Boolean = false
|
||||||
|
|
||||||
|
val participantRemovedEvent: MutableLiveData<Event<GroupChatRoomMember>> by lazy {
|
||||||
|
MutableLiveData<Event<GroupChatRoomMember>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding: ChatRoomGroupInfoParticipantCellBinding = DataBindingUtil.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
R.layout.chat_room_group_info_participant_cell, parent, false
|
||||||
|
)
|
||||||
|
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<GroupChatRoomMember>() {
|
||||||
|
override fun areItemsTheSame(
|
||||||
|
oldItem: GroupChatRoomMember,
|
||||||
|
newItem: GroupChatRoomMember
|
||||||
|
): Boolean {
|
||||||
|
return oldItem.address.weakEqual(newItem.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(
|
||||||
|
oldItem: GroupChatRoomMember,
|
||||||
|
newItem: GroupChatRoomMember
|
||||||
|
): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.adapters
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.databinding.DataBindingUtil
|
||||||
|
import androidx.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<ParticipantImdnState,
|
||||||
|
ImdnAdapter.ViewHolder>(ParticipantImdnStateDiffCallback()), HeaderAdapter {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val binding: ChatRoomImdnParticipantCellBinding = DataBindingUtil.inflate(
|
||||||
|
LayoutInflater.from(parent.context),
|
||||||
|
R.layout.chat_room_imdn_participant_cell, parent, false
|
||||||
|
)
|
||||||
|
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<ParticipantImdnState>() {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import 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<Address>
|
||||||
|
if (participants != null && participants.size > 0) {
|
||||||
|
viewModel.selectedAddresses.value = participants
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Int>) {
|
||||||
|
val list = ArrayList<EventLog>()
|
||||||
|
for (index in indexesOfItemToDelete) {
|
||||||
|
val eventLog = adapter.getItemAt(index)
|
||||||
|
list.add(eventLog)
|
||||||
|
}
|
||||||
|
listViewModel.deleteEventLogs(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(
|
||||||
|
requestCode: Int,
|
||||||
|
permissions: Array<out String>,
|
||||||
|
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<Intent>()
|
||||||
|
|
||||||
|
// 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<Parcelable>()))
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Address>()
|
||||||
|
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<Address>
|
||||||
|
if (participants != null && participants.size > 0) {
|
||||||
|
val list = arrayListOf<GroupChatRoomMember>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.fragments
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Int>) {
|
||||||
|
val list = ArrayList<ChatRoom>()
|
||||||
|
for (index in indexesOfItemToDelete) {
|
||||||
|
val chatRoom = adapter.getItemAt(index)
|
||||||
|
list.add(chatRoom)
|
||||||
|
}
|
||||||
|
listViewModel.deleteChatRooms(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun scrollToTop() {
|
||||||
|
binding.chatList.scrollToPosition(0)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
* This file is part of linphone-android
|
||||||
* (see https://www.linphone.org).
|
* (see https://www.linphone.org).
|
||||||
|
@ -17,19 +17,19 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.linphone.chat;
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
import android.view.View;
|
import androidx.lifecycle.ViewModel
|
||||||
import android.widget.ImageView;
|
import org.linphone.utils.FileUtils
|
||||||
import android.widget.TextView;
|
|
||||||
import org.linphone.R;
|
|
||||||
|
|
||||||
class DeviceChildViewHolder {
|
class ChatMessageAttachmentViewModel(
|
||||||
public final TextView deviceName;
|
val path: String,
|
||||||
public final ImageView securityLevel;
|
val isImage: Boolean,
|
||||||
|
private val deleteCallback: (attachment: ChatMessageAttachmentViewModel) -> Unit
|
||||||
|
) : ViewModel() {
|
||||||
|
val fileName: String = FileUtils.getNameFromFilePath(path)
|
||||||
|
|
||||||
public DeviceChildViewHolder(View v) {
|
fun delete() {
|
||||||
deviceName = v.findViewById(R.id.name);
|
deleteCallback(this)
|
||||||
securityLevel = v.findViewById(R.id.security_level);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Boolean>()
|
||||||
|
|
||||||
|
val downloadable = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val downloadEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import java.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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return ChatMessageSendingViewModel(chatRoom) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
|
var temporaryFileUploadPath: File? = null
|
||||||
|
|
||||||
|
val attachments = MutableLiveData<ArrayList<ChatMessageAttachmentViewModel>>()
|
||||||
|
|
||||||
|
val attachFileEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val sendMessageEnabled = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isReadOnly = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
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<ChatMessageAttachmentViewModel>()
|
||||||
|
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<ChatMessageAttachmentViewModel>()
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Boolean>()
|
||||||
|
|
||||||
|
val transferInProgress = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val showImdn = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val imdnIcon = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
val backgroundRes = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
val hideAvatar = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val hideTime = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val contents = MutableLiveData<ArrayList<ChatMessageContentViewModel>>()
|
||||||
|
|
||||||
|
val time = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val ephemeralLifetime = MutableLiveData<String>()
|
||||||
|
|
||||||
|
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<ChatMessageContentViewModel>()
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import java.util.*
|
||||||
|
import 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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return ChatMessagesListViewModel(chatRoom) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
|
companion object {
|
||||||
|
private const val MESSAGES_PER_PAGE = 20
|
||||||
|
}
|
||||||
|
|
||||||
|
val events = MutableLiveData<ArrayList<EventLog>>()
|
||||||
|
|
||||||
|
val messageUpdatedEvent: MutableLiveData<Event<Int>> by lazy {
|
||||||
|
MutableLiveData<Event<Int>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val requestWriteExternalStoragePermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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<EventLog>()
|
||||||
|
list.addAll(events.value.orEmpty())
|
||||||
|
list.removeAt(position)
|
||||||
|
events.value = list
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteEventLogs(listToDelete: ArrayList<EventLog>) {
|
||||||
|
val list = arrayListOf<EventLog>()
|
||||||
|
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<EventLog> = chatRoom.getHistoryRangeEvents(totalItemsCount, upperBound)
|
||||||
|
val list = arrayListOf<EventLog>()
|
||||||
|
for (message in history) {
|
||||||
|
list.add(message)
|
||||||
|
}
|
||||||
|
list.addAll(events.value.orEmpty())
|
||||||
|
events.value = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addEvent(eventLog: EventLog) {
|
||||||
|
val list = arrayListOf<EventLog>()
|
||||||
|
list.addAll(events.value.orEmpty())
|
||||||
|
list.add(eventLog)
|
||||||
|
events.value = list
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getEvents(): ArrayList<EventLog> {
|
||||||
|
val list = arrayListOf<EventLog>()
|
||||||
|
val history = chatRoom.getHistoryEvents(MESSAGES_PER_PAGE)
|
||||||
|
for (message in history) {
|
||||||
|
list.add(message)
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
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<Contact>()
|
||||||
|
|
||||||
|
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<Boolean> by lazy {
|
||||||
|
MutableLiveData<Boolean>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val isSelected: MutableLiveData<Boolean> by lazy {
|
||||||
|
MutableLiveData<Boolean>()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.activities.main.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<Event<ChatRoom>> by lazy {
|
||||||
|
MutableLiveData<Event<ChatRoom>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val createGroupChat = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val sipContactsSelected = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val isEncrypted = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val contactsList = MutableLiveData<ArrayList<SearchResult>>()
|
||||||
|
|
||||||
|
val waitForChatRoomCreation = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val selectedAddresses = MutableLiveData<ArrayList<Address>>()
|
||||||
|
|
||||||
|
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<SearchResult>()
|
||||||
|
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<Address>()
|
||||||
|
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<Address>(1)
|
||||||
|
participants[0] = searchResult.address
|
||||||
|
|
||||||
|
room = coreContext.core.createChatRoom(
|
||||||
|
params,
|
||||||
|
AppUtils.getString(R.string.chat_room_dummy_subject),
|
||||||
|
participants
|
||||||
|
)
|
||||||
|
room?.addListener(listener)
|
||||||
|
} else {
|
||||||
|
room = coreContext.core.getChatRoom(searchResult.address, defaultProxyConfig.identityAddress)
|
||||||
|
if (room != null) {
|
||||||
|
chatRoomCreatedEvent.value = Event(room)
|
||||||
|
} else {
|
||||||
|
Log.e("[Chat Room Creation] Couldn't create chat room with remote ${searchResult.address.asStringUriOnly()} and local identity ${defaultProxyConfig.identityAddress.asStringUriOnly()}")
|
||||||
|
}
|
||||||
|
waitForChatRoomCreation.value = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i("[Chat Room Creation] Found existing 1-1 chat room with remote ${searchResult.address.asStringUriOnly()}, encryption=$encrypted and local identity ${defaultProxyConfig.identityAddress.asStringUriOnly()}")
|
||||||
|
chatRoomCreatedEvent.value = Event(room)
|
||||||
|
waitForChatRoomCreation.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,292 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||||
|
*
|
||||||
|
* This file is part of linphone-android
|
||||||
|
* (see https://www.linphone.org).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.contact.Contact
|
||||||
|
import org.linphone.contact.ContactViewModelInterface
|
||||||
|
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||||
|
import org.linphone.core.*
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.AppUtils
|
||||||
|
import org.linphone.utils.LinphoneUtils
|
||||||
|
import org.linphone.utils.TimestampUtils
|
||||||
|
|
||||||
|
class ChatRoomViewModelFactory(private val chatRoom: ChatRoom) :
|
||||||
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return ChatRoomViewModel(chatRoom) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactViewModelInterface {
|
||||||
|
override val contact = MutableLiveData<Contact>()
|
||||||
|
|
||||||
|
override val displayName: String by lazy {
|
||||||
|
when {
|
||||||
|
chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()) -> LinphoneUtils.getDisplayName(chatRoom.peerAddress)
|
||||||
|
chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) -> LinphoneUtils.getDisplayName(chatRoom.participants.first()?.address ?: chatRoom.peerAddress)
|
||||||
|
chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) -> chatRoom.subject
|
||||||
|
else -> chatRoom.peerAddress.asStringUriOnly()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val securityLevel: ChatRoomSecurityLevel
|
||||||
|
get() = chatRoom.securityLevel
|
||||||
|
|
||||||
|
override val showGroupChatAvatar: Boolean
|
||||||
|
get() = chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) && !chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
||||||
|
|
||||||
|
val subject = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val participants = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val unreadMessagesCount = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
val lastUpdate = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val lastMessageText = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val callInProgress = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val remoteIsComposing = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val composingList = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val securityLevelIcon = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
val securityLevelContentDescription = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
val oneToOneChatRoom: Boolean
|
||||||
|
get() = chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
||||||
|
|
||||||
|
val encryptedChatRoom: Boolean
|
||||||
|
get() = chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())
|
||||||
|
|
||||||
|
val basicChatRoom: Boolean
|
||||||
|
get() = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||||
|
|
||||||
|
val peerSipUri: String
|
||||||
|
get() = chatRoom.peerAddress.asStringUriOnly()
|
||||||
|
|
||||||
|
val oneParticipantOneDevice: Boolean
|
||||||
|
get() {
|
||||||
|
return chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) &&
|
||||||
|
chatRoom.me.devices.size == 1 &&
|
||||||
|
chatRoom.participants.first().devices.size == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
val addressToCall: Address
|
||||||
|
get() {
|
||||||
|
return if (chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()))
|
||||||
|
chatRoom.peerAddress
|
||||||
|
else
|
||||||
|
chatRoom.participants.first().address
|
||||||
|
}
|
||||||
|
|
||||||
|
val onlyParticipantOnlyDeviceAddress: Address
|
||||||
|
get() = chatRoom.participants.first().devices.first().address
|
||||||
|
|
||||||
|
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
|
||||||
|
override fun onContactsUpdated() {
|
||||||
|
Log.i("[Chat Room] Contacts have changed")
|
||||||
|
contactLookup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
|
||||||
|
override fun onCallStateChanged(
|
||||||
|
core: Core,
|
||||||
|
call: Call,
|
||||||
|
state: Call.State,
|
||||||
|
message: String
|
||||||
|
) {
|
||||||
|
callInProgress.value = core.callsNb > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||||
|
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||||
|
Log.i("[Chat Room] $chatRoom state changed: $state")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
subject.value = chatRoom.subject
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
unreadMessagesCount.value = chatRoom.unreadMessagesCount
|
||||||
|
lastMessageText.value = formatLastMessage(eventLog.chatMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
lastMessageText.value = formatLastMessage(eventLog.chatMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
contactLookup()
|
||||||
|
updateSecurityIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
contactLookup()
|
||||||
|
updateSecurityIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIsComposingReceived(
|
||||||
|
chatRoom: ChatRoom,
|
||||||
|
remoteAddr: Address,
|
||||||
|
isComposing: Boolean
|
||||||
|
) {
|
||||||
|
updateRemotesComposing()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
contactLookup()
|
||||||
|
updateSecurityIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
updateSecurityIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onParticipantDeviceAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
updateSecurityIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
updateSecurityIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||||
|
Log.i("[Chat Room] Ephemeral message deleted, updated last message displayed")
|
||||||
|
lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
chatRoom.core.addListener(coreListener)
|
||||||
|
chatRoom.addListener(chatRoomListener)
|
||||||
|
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
||||||
|
|
||||||
|
lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
|
||||||
|
unreadMessagesCount.value = chatRoom.unreadMessagesCount
|
||||||
|
lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true)
|
||||||
|
|
||||||
|
subject.value = chatRoom.subject
|
||||||
|
updateSecurityIcon()
|
||||||
|
|
||||||
|
contactLookup()
|
||||||
|
|
||||||
|
callInProgress.value = chatRoom.core.callsNb > 0
|
||||||
|
updateRemotesComposing()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
||||||
|
chatRoom.removeListener(chatRoomListener)
|
||||||
|
chatRoom.core.removeListener(coreListener)
|
||||||
|
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun contactLookup() {
|
||||||
|
if (chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
|
||||||
|
searchMatchingContact()
|
||||||
|
} else {
|
||||||
|
getParticipantsNames()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatLastMessage(msg: ChatMessage?): String {
|
||||||
|
if (msg == null) return ""
|
||||||
|
|
||||||
|
val sender: String =
|
||||||
|
coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.fullName
|
||||||
|
?: LinphoneUtils.getDisplayName(msg.fromAddress)
|
||||||
|
var body = ""
|
||||||
|
for (content in msg.contents) {
|
||||||
|
if (content.isFile || content.isFileTransfer) body += content.name + " "
|
||||||
|
else if (content.isText) body += content.stringBuffer + " "
|
||||||
|
}
|
||||||
|
|
||||||
|
return "$sender: $body"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchMatchingContact() {
|
||||||
|
val remoteAddress = if (chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())) {
|
||||||
|
chatRoom.peerAddress
|
||||||
|
} else {
|
||||||
|
if (chatRoom.participants.isNotEmpty()) {
|
||||||
|
chatRoom.participants[0].address
|
||||||
|
} else {
|
||||||
|
Log.e("[Chat Room] $chatRoom doesn't have any participant in state ${chatRoom.state}!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
contact.value = coreContext.contactsManager.findContactByAddress(remoteAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getParticipantsNames() {
|
||||||
|
if (oneToOneChatRoom) return
|
||||||
|
|
||||||
|
var participantsList = ""
|
||||||
|
var index = 0
|
||||||
|
for (participant in chatRoom.participants) {
|
||||||
|
val contact: Contact? =
|
||||||
|
coreContext.contactsManager.findContactByAddress(participant.address)
|
||||||
|
participantsList += contact?.fullName ?: LinphoneUtils.getDisplayName(participant.address)
|
||||||
|
index++
|
||||||
|
if (index != chatRoom.nbParticipants) participantsList += ", "
|
||||||
|
}
|
||||||
|
participants.value = participantsList
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateSecurityIcon() {
|
||||||
|
securityLevelIcon.value = when (chatRoom.securityLevel) {
|
||||||
|
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||||
|
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||||
|
else -> R.drawable.security_alert_indicator
|
||||||
|
}
|
||||||
|
securityLevelContentDescription.value = when (chatRoom.securityLevel) {
|
||||||
|
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||||
|
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||||
|
else -> R.string.content_description_security_level_unsafe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRemotesComposing() {
|
||||||
|
remoteIsComposing.value = chatRoom.isRemoteComposing
|
||||||
|
|
||||||
|
var composing = ""
|
||||||
|
for (address in chatRoom.composingAddresses) {
|
||||||
|
val contact: Contact? = coreContext.contactsManager.findContactByAddress(address)
|
||||||
|
composing += if (composing.isNotEmpty()) ", " else ""
|
||||||
|
composing += contact?.fullName ?: LinphoneUtils.getDisplayName(address)
|
||||||
|
}
|
||||||
|
composingList.value = AppUtils.getStringWithPlural(R.plurals.chat_room_remote_composing, chatRoom.composingAddresses.size, composing)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||||
|
*
|
||||||
|
* This file is part of linphone-android
|
||||||
|
* (see https://www.linphone.org).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.activities.main.viewmodels.ErrorReportingViewModel
|
||||||
|
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||||
|
import org.linphone.core.*
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.Event
|
||||||
|
import org.linphone.utils.LinphoneUtils
|
||||||
|
|
||||||
|
class ChatRoomsListViewModel : ErrorReportingViewModel() {
|
||||||
|
val chatRooms = MutableLiveData<ArrayList<ChatRoom>>()
|
||||||
|
|
||||||
|
val latestUpdatedChatRoomId = MutableLiveData<Int>()
|
||||||
|
|
||||||
|
val contactsUpdatedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||||
|
MutableLiveData<Event<Boolean>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupChatAvailable: Boolean = LinphoneUtils.isGroupChatAvailable()
|
||||||
|
|
||||||
|
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
|
||||||
|
override fun onContactsUpdated() {
|
||||||
|
Log.i("[Chat Rooms] Contacts have changed")
|
||||||
|
contactsUpdatedEvent.value = Event(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||||
|
override fun onChatRoomStateChanged(core: Core, chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||||
|
if (state == ChatRoom.State.Created) {
|
||||||
|
updateChatRooms()
|
||||||
|
} else if (state == ChatRoom.State.TerminationFailed) {
|
||||||
|
Log.e("[Chat Rooms] Group chat room removal for address ${chatRoom.peerAddress.asStringUriOnly()} has failed !")
|
||||||
|
onErrorEvent.value = Event(R.string.chat_room_removal_failed_snack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChatRoomSubjectChanged(core: Core, chatRoom: ChatRoom) {
|
||||||
|
updateChatRoom(chatRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
|
||||||
|
updateChatRoom(chatRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||||
|
if (chatRooms.value?.indexOf(chatRoom) == 0) updateChatRoom(chatRoom)
|
||||||
|
else updateChatRooms()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||||
|
if (chatRooms.value?.indexOf(chatRoom) == 0) updateChatRoom(chatRoom)
|
||||||
|
else updateChatRooms()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMessageReceivedUnableDecrypt(
|
||||||
|
core: Core,
|
||||||
|
chatRoom: ChatRoom,
|
||||||
|
message: ChatMessage
|
||||||
|
) {
|
||||||
|
updateChatRooms()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||||
|
override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State) {
|
||||||
|
if (newState == ChatRoom.State.Deleted) {
|
||||||
|
val list = arrayListOf<ChatRoom>()
|
||||||
|
list.addAll(chatRooms.value.orEmpty())
|
||||||
|
list.remove(chatRoom)
|
||||||
|
chatRooms.value = list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var chatRoomsToDeleteCount = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
chatRooms.value = getChatRooms()
|
||||||
|
coreContext.core.addListener(listener)
|
||||||
|
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
||||||
|
coreContext.core.removeListener(listener)
|
||||||
|
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteChatRoom(chatRoom: ChatRoom?) {
|
||||||
|
for (eventLog in chatRoom?.getHistoryMessageEvents(0).orEmpty()) {
|
||||||
|
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
chatRoomsToDeleteCount = 1
|
||||||
|
chatRoom?.addListener(chatRoomListener)
|
||||||
|
chatRoom?.core?.deleteChatRoom(chatRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteChatRooms(chatRooms: ArrayList<ChatRoom>) {
|
||||||
|
chatRoomsToDeleteCount = chatRooms.size
|
||||||
|
for (chatRoom in chatRooms) {
|
||||||
|
for (eventLog in chatRoom.getHistoryMessageEvents(0).orEmpty()) {
|
||||||
|
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||||
|
}
|
||||||
|
|
||||||
|
chatRoom.addListener(chatRoomListener)
|
||||||
|
chatRoom.core?.deleteChatRoom(chatRoom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateChatRoom(chatRoom: ChatRoom) {
|
||||||
|
latestUpdatedChatRoomId.value = chatRooms.value?.indexOf(chatRoom)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateChatRooms() {
|
||||||
|
chatRooms.value = getChatRooms()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getChatRooms(): ArrayList<ChatRoom> {
|
||||||
|
val list = arrayListOf<ChatRoom>()
|
||||||
|
|
||||||
|
for (room in coreContext.core.chatRooms) {
|
||||||
|
list.add(room)
|
||||||
|
}
|
||||||
|
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||||
|
*
|
||||||
|
* This file is part of linphone-android
|
||||||
|
* (see https://www.linphone.org).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.core.ChatRoomSecurityLevel
|
||||||
|
import org.linphone.core.ParticipantDevice
|
||||||
|
|
||||||
|
class DevicesListChildViewModel(private val device: ParticipantDevice) : ViewModel() {
|
||||||
|
val deviceName: String = device.name
|
||||||
|
|
||||||
|
val securityLevelIcon: Int by lazy {
|
||||||
|
when (device.securityLevel) {
|
||||||
|
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||||
|
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||||
|
else -> R.drawable.security_alert_indicator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val securityContentDescription: Int by lazy {
|
||||||
|
when (device.securityLevel) {
|
||||||
|
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||||
|
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||||
|
else -> R.string.content_description_security_level_unsafe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClick() {
|
||||||
|
coreContext.startCall(device.address, true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||||
|
*
|
||||||
|
* This file is part of linphone-android
|
||||||
|
* (see https://www.linphone.org).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.contact.GenericContactViewModel
|
||||||
|
import org.linphone.core.ChatRoomSecurityLevel
|
||||||
|
import org.linphone.core.Participant
|
||||||
|
|
||||||
|
class DevicesListGroupViewModel(private val participant: Participant) : GenericContactViewModel(participant.address) {
|
||||||
|
override val securityLevel: ChatRoomSecurityLevel
|
||||||
|
get() = participant.securityLevel
|
||||||
|
|
||||||
|
private val device = if (participant.devices.isEmpty()) null else participant.devices.first()
|
||||||
|
|
||||||
|
val securityLevelIcon: Int by lazy {
|
||||||
|
when (device?.securityLevel) {
|
||||||
|
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||||
|
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||||
|
else -> R.drawable.security_alert_indicator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val securityLevelContentDescription: Int by lazy {
|
||||||
|
when (device?.securityLevel) {
|
||||||
|
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||||
|
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||||
|
else -> R.string.content_description_security_level_unsafe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sipUri: String = participant.address.asStringUriOnly()
|
||||||
|
|
||||||
|
val isExpanded = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
|
val devices = MutableLiveData<ArrayList<DevicesListChildViewModel>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
isExpanded.value = false
|
||||||
|
|
||||||
|
val list = arrayListOf<DevicesListChildViewModel>()
|
||||||
|
for (device in participant.devices) {
|
||||||
|
list.add(DevicesListChildViewModel((device)))
|
||||||
|
}
|
||||||
|
devices.value = list
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleExpanded() {
|
||||||
|
isExpanded.value = isExpanded.value != true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onClick() {
|
||||||
|
if (device?.address != null) coreContext.startCall(device.address, true)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.core.ChatRoom
|
||||||
|
import org.linphone.core.ChatRoomListenerStub
|
||||||
|
import org.linphone.core.EventLog
|
||||||
|
|
||||||
|
class DevicesListViewModelFactory(private val chatRoom: ChatRoom) :
|
||||||
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return DevicesListViewModel(chatRoom) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
|
val participants = MutableLiveData<ArrayList<DevicesListGroupViewModel>>()
|
||||||
|
|
||||||
|
private val listener = object : ChatRoomListenerStub() {
|
||||||
|
override fun onParticipantDeviceAdded(chatRoom: ChatRoom?, eventLog: EventLog?) {
|
||||||
|
updateParticipants()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom?, eventLog: EventLog?) {
|
||||||
|
updateParticipants()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onParticipantAdded(chatRoom: ChatRoom?, eventLog: EventLog?) {
|
||||||
|
updateParticipants()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onParticipantRemoved(chatRoom: ChatRoom?, eventLog: EventLog?) {
|
||||||
|
updateParticipants()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
chatRoom.addListener(listener)
|
||||||
|
updateParticipants()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
chatRoom.removeListener(listener)
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateParticipants() {
|
||||||
|
val list = arrayListOf<DevicesListGroupViewModel>()
|
||||||
|
list.add(DevicesListGroupViewModel(chatRoom.me))
|
||||||
|
for (participant in chatRoom.participants) {
|
||||||
|
list.add(DevicesListGroupViewModel(participant))
|
||||||
|
}
|
||||||
|
participants.value = list
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
* This file is part of linphone-android
|
||||||
* (see https://www.linphone.org).
|
* (see https://www.linphone.org).
|
||||||
|
@ -17,21 +17,23 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
package org.linphone.call;
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
import android.view.View;
|
import androidx.lifecycle.ViewModel
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import org.linphone.R;
|
|
||||||
|
|
||||||
public class CallStatsViewHolder {
|
class EphemeralDurationViewModel(
|
||||||
|
val textResource: Int,
|
||||||
|
private val selectedDuration: Long,
|
||||||
|
private val duration: Long,
|
||||||
|
private val listener: DurationItemClicked
|
||||||
|
) : ViewModel() {
|
||||||
|
val selected: Boolean = selectedDuration == duration
|
||||||
|
|
||||||
public final RelativeLayout avatarLayout;
|
fun setSelected() {
|
||||||
public final TextView participantName, sipUri;
|
listener.onDurationValueChanged(duration)
|
||||||
|
|
||||||
public CallStatsViewHolder(View v) {
|
|
||||||
avatarLayout = v.findViewById(R.id.avatar_layout);
|
|
||||||
participantName = v.findViewById(R.id.name);
|
|
||||||
sipUri = v.findViewById(R.id.sipUri);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DurationItemClicked {
|
||||||
|
fun onDurationValueChanged(duration: Long)
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||||
|
*
|
||||||
|
* This file is part of linphone-android
|
||||||
|
* (see https://www.linphone.org).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.core.ChatRoom
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
|
||||||
|
class EphemeralViewModelFactory(private val chatRoom: ChatRoom) :
|
||||||
|
ViewModelProvider.NewInstanceFactory() {
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||||
|
return EphemeralViewModel(chatRoom) as T
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||||
|
val durationsList = MutableLiveData<ArrayList<EphemeralDurationViewModel>>()
|
||||||
|
|
||||||
|
var currentSelectedDuration: Long = 0
|
||||||
|
|
||||||
|
private val listener = object : DurationItemClicked {
|
||||||
|
override fun onDurationValueChanged(duration: Long) {
|
||||||
|
currentSelectedDuration = duration
|
||||||
|
computeEphemeralDurationValues()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
Log.i("[Ephemeral Messages] Current duration is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.ephemeralEnabled()}")
|
||||||
|
currentSelectedDuration = if (chatRoom.ephemeralEnabled()) chatRoom.ephemeralLifetime else 0
|
||||||
|
computeEphemeralDurationValues()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateChatRoomEphemeralDuration() {
|
||||||
|
Log.i("[Ephemeral Messages] Selected value is $currentSelectedDuration")
|
||||||
|
if (currentSelectedDuration > 0) {
|
||||||
|
if (chatRoom.ephemeralLifetime != currentSelectedDuration) {
|
||||||
|
Log.i("[Ephemeral Messages] Setting new lifetime for ephemeral messages to $currentSelectedDuration")
|
||||||
|
chatRoom.ephemeralLifetime = currentSelectedDuration
|
||||||
|
} else {
|
||||||
|
Log.i("[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chatRoom.ephemeralEnabled()) {
|
||||||
|
Log.i("[Ephemeral Messages] Ephemeral messages were disabled, enable them")
|
||||||
|
chatRoom.enableEphemeral(true)
|
||||||
|
}
|
||||||
|
} else if (chatRoom.ephemeralEnabled()) {
|
||||||
|
Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them")
|
||||||
|
chatRoom.enableEphemeral(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeEphemeralDurationValues() {
|
||||||
|
val list = arrayListOf<EphemeralDurationViewModel>()
|
||||||
|
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_disabled, currentSelectedDuration, 0, listener))
|
||||||
|
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_minute, currentSelectedDuration, 60, listener))
|
||||||
|
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_hour, currentSelectedDuration, 3600, listener))
|
||||||
|
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_day, currentSelectedDuration, 86400, listener))
|
||||||
|
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_three_days, currentSelectedDuration, 259200, listener))
|
||||||
|
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_week, currentSelectedDuration, 604800, listener))
|
||||||
|
durationsList.value = list
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||||
|
*
|
||||||
|
* This file is part of linphone-android
|
||||||
|
* (see https://www.linphone.org).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package org.linphone.activities.main.chat.viewmodels
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||||
|
import org.linphone.R
|
||||||
|
import org.linphone.contact.Contact
|
||||||
|
import org.linphone.core.EventLog
|
||||||
|
import org.linphone.core.tools.Log
|
||||||
|
import org.linphone.utils.LinphoneUtils
|
||||||
|
|
||||||
|
class EventViewModel(private val eventLog: EventLog) : ViewModel() {
|
||||||
|
val text = MutableLiveData<String>()
|
||||||
|
|
||||||
|
val isSecurity: Boolean by lazy {
|
||||||
|
when (eventLog.type) {
|
||||||
|
EventLog.Type.ConferenceSecurityEvent -> true
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val contact: Contact? by lazy {
|
||||||
|
val address = eventLog.participantAddress ?: eventLog.securityEventFaultyDeviceAddress
|
||||||
|
if (address != null) {
|
||||||
|
coreContext.contactsManager.findContactByAddress(address)
|
||||||
|
} else {
|
||||||
|
Log.e("[Event ViewModel] Unexpected null address for event $eventLog")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val displayName: String by lazy {
|
||||||
|
val address = eventLog.participantAddress ?: eventLog.securityEventFaultyDeviceAddress
|
||||||
|
if (address != null) {
|
||||||
|
LinphoneUtils.getDisplayName(address)
|
||||||
|
} else {
|
||||||
|
Log.e("[Event ViewModel] Unexpected null address for event $eventLog")
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
updateEventText()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getName(): String {
|
||||||
|
return contact?.fullName ?: displayName
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateEventText() {
|
||||||
|
val context: Context = coreContext.context
|
||||||
|
|
||||||
|
text.value = when (eventLog.type) {
|
||||||
|
EventLog.Type.ConferenceCreated -> context.getString(R.string.chat_event_conference_created)
|
||||||
|
EventLog.Type.ConferenceTerminated -> context.getString(R.string.chat_event_conference_destroyed)
|
||||||
|
EventLog.Type.ConferenceParticipantAdded -> context.getString(R.string.chat_event_participant_added).format(getName())
|
||||||
|
EventLog.Type.ConferenceParticipantRemoved -> context.getString(R.string.chat_event_participant_removed).format(getName())
|
||||||
|
EventLog.Type.ConferenceSubjectChanged -> context.getString(R.string.chat_event_subject_changed).format(eventLog.subject)
|
||||||
|
EventLog.Type.ConferenceParticipantSetAdmin -> context.getString(R.string.chat_event_admin_set).format(getName())
|
||||||
|
EventLog.Type.ConferenceParticipantUnsetAdmin -> context.getString(R.string.chat_event_admin_unset).format(getName())
|
||||||
|
EventLog.Type.ConferenceParticipantDeviceAdded -> context.getString(R.string.chat_event_device_added).format(getName())
|
||||||
|
EventLog.Type.ConferenceParticipantDeviceRemoved -> context.getString(R.string.chat_event_device_removed).format(getName())
|
||||||
|
EventLog.Type.ConferenceSecurityEvent -> {
|
||||||
|
val name = getName()
|
||||||
|
when (eventLog.securityEventType) {
|
||||||
|
EventLog.SecurityEventType.EncryptionIdentityKeyChanged -> context.getString(R.string.chat_security_event_lime_identity_key_changed).format(name)
|
||||||
|
EventLog.SecurityEventType.ManInTheMiddleDetected -> context.getString(R.string.chat_security_event_man_in_the_middle_detected).format(name)
|
||||||
|
EventLog.SecurityEventType.SecurityLevelDowngraded -> context.getString(R.string.chat_security_event_security_level_downgraded).format(name)
|
||||||
|
EventLog.SecurityEventType.ParticipantMaxDeviceCountExceeded -> context.getString(R.string.chat_security_event_participant_max_count_exceeded).format(name)
|
||||||
|
else -> "Unexpected security event for $name: ${eventLog.securityEventType}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventLog.Type.ConferenceEphemeralMessageDisabled -> context.getString(R.string.chat_event_ephemeral_disabled)
|
||||||
|
EventLog.Type.ConferenceEphemeralMessageEnabled -> context.getString(R.string.chat_event_ephemeral_enabled).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||||
|
EventLog.Type.ConferenceEphemeralMessageLifetimeChanged -> context.getString(R.string.chat_event_ephemeral_lifetime_changed).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||||
|
else -> "Unexpected event: ${eventLog.type}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatEphemeralExpiration(context: Context, duration: Long): String {
|
||||||
|
return when (duration) {
|
||||||
|
0L -> context.getString(R.string.chat_room_ephemeral_message_disabled)
|
||||||
|
60L -> context.getString(R.string.chat_room_ephemeral_message_one_minute)
|
||||||
|
3600L -> context.getString(R.string.chat_room_ephemeral_message_one_hour)
|
||||||
|
86400L -> context.getString(R.string.chat_room_ephemeral_message_one_day)
|
||||||
|
259200L -> context.getString(R.string.chat_room_ephemeral_message_three_days)
|
||||||
|
604800L -> context.getString(R.string.chat_room_ephemeral_message_one_week)
|
||||||
|
else -> "Unexpected duration"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue