Kotlin rewrite

This commit is contained in:
Sylvain Berfini 2020-03-29 14:14:04 +02:00
parent 1198551254
commit 5f1984fe4b
727 changed files with 35466 additions and 59669 deletions

View file

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

View file

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

View file

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

245
app/src/main/AndroidManifest.xml Executable file → Normal file
View 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>

View file

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

View file

@ -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
@ -35,3 +38,5 @@ 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

View file

@ -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
@ -37,3 +39,5 @@ xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
[lime] [lime]
lime_update_threshold=-1 lime_update_threshold=-1
## End of factory rc

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

@ -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 its still loading, we check to see if the data set count has
// changed, if so we conclude it has finished loading and update the current page
// number and total item count.
if (loading && totalItemCount > previousTotalItemCount) {
loading = false
previousTotalItemCount = totalItemCount
}
// If it isnt currently loading, we check to see if we have breached
// the mVisibleThreshold and need to reload more data.
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
// threshold should reflect how many total columns there are too
if (!loading && firstVisibleItemPosition < mVisibleThreshold && firstVisibleItemPosition > 0 && lastVisibleItemPosition < totalItemCount - mVisibleThreshold) {
onLoadMore(totalItemCount)
loading = true
}
}
// Defines the process for actually loading more data based on page
protected abstract fun onLoadMore(totalItemsCount: Int)
companion object {
// The minimum amount of items to have below your current scroll position
// before loading more.
private const val mVisibleThreshold = 5
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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