Kotlin rewrite
This commit is contained in:
parent
1198551254
commit
5f1984fe4b
727 changed files with 35466 additions and 59669 deletions
136
app/build.gradle
136
app/build.gradle
|
@ -1,5 +1,13 @@
|
|||
apply plugin: 'com.android.application'
|
||||
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
apply plugin: "org.jlleitschuh.gradle.ktlint"
|
||||
|
||||
static def getPackageName() {
|
||||
return "org.linphone"
|
||||
}
|
||||
|
@ -10,7 +18,7 @@ static def firebaseEnabled() {
|
|||
}
|
||||
|
||||
task getGitVersion() {
|
||||
def gitVersion = "4.4.0"
|
||||
def gitVersion = "5.0"
|
||||
def gitVersionStream = new ByteArrayOutputStream()
|
||||
def gitCommitsCount = new ByteArrayOutputStream()
|
||||
def gitCommitHash = new ByteArrayOutputStream()
|
||||
|
@ -41,69 +49,18 @@ task getGitVersion() {
|
|||
project.version = gitVersion
|
||||
}
|
||||
|
||||
configurations {
|
||||
customImpl.extendsFrom(implementation)
|
||||
}
|
||||
|
||||
task linphoneSdkSource() {
|
||||
doLast {
|
||||
configurations.customImpl.getIncoming().each {
|
||||
it.getResolutionResult().allComponents.each {
|
||||
if (it.id.getDisplayName().contains("linphone-sdk-android")) {
|
||||
println 'Linphone SDK used is ' + it.moduleVersion.version + ' from ' + it.properties["repositoryName"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///// Exclude Files /////
|
||||
|
||||
def excludeFiles = []
|
||||
if (!firebaseEnabled()) {
|
||||
excludeFiles.add('**/Firebase*')
|
||||
println '[Push Notification] Firebase disabled'
|
||||
}
|
||||
// Remove or comment if you want to use those
|
||||
excludeFiles.add('**/XmlRpc*')
|
||||
excludeFiles.add('**/InAppPurchase*')
|
||||
|
||||
def excludePackage = []
|
||||
|
||||
excludePackage.add('**/gdb.*')
|
||||
excludePackage.add('**/libopenh264**')
|
||||
excludePackage.add('**/**tester**')
|
||||
excludePackage.add('**/LICENSE.txt')
|
||||
|
||||
/////////////////////////
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
name "local linphone-sdk maven repository"
|
||||
url file(LinphoneSdkBuildDir + '/maven_repository/')
|
||||
}
|
||||
maven {
|
||||
name "linphone.org maven repository"
|
||||
url "https://linphone.org/maven_repository"
|
||||
}
|
||||
}
|
||||
|
||||
project.tasks['preBuild'].dependsOn 'getGitVersion'
|
||||
project.tasks['preBuild'].dependsOn 'linphoneSdkSource'
|
||||
|
||||
android {
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.2"
|
||||
defaultConfig {
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode 4400
|
||||
versionCode 4300
|
||||
versionName "${project.version}"
|
||||
applicationId getPackageName()
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
|
@ -114,12 +71,10 @@ android {
|
|||
// https://developer.android.com/studio/releases/gradle-plugin#3-6-0-behavior for extractNativeLibs
|
||||
if (variant.buildType.name == "release") {
|
||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
||||
linphone_file_provider: getPackageName() + ".provider",
|
||||
extractNativeLibs: "false"]
|
||||
linphone_file_provider: getPackageName() + ".fileprovider"]
|
||||
} else {
|
||||
variant.getMergedFlavor().manifestPlaceholders = [linphone_address_mime_type: "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address",
|
||||
linphone_file_provider: getPackageName() + ".debug.provider",
|
||||
extractNativeLibs: "true"]
|
||||
linphone_file_provider: getPackageName() + ".debug.fileprovider"]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -143,20 +98,21 @@ android {
|
|||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
|
||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
||||
resValue "string", "file_provider", getPackageName() + ".provider"
|
||||
resValue "string", "file_provider", getPackageName() + ".fileprovider"
|
||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
||||
|
||||
if (!firebaseEnabled()) {
|
||||
resValue "string", "gcm_defaultSenderId", "none"
|
||||
}
|
||||
}
|
||||
|
||||
debug {
|
||||
applicationIdSuffix ".debug"
|
||||
debuggable true
|
||||
jniDebuggable true
|
||||
|
||||
resValue "string", "sync_account_type", getPackageName() + ".sync"
|
||||
resValue "string", "file_provider", getPackageName() + ".debug.provider"
|
||||
resValue "string", "file_provider", getPackageName() + ".debug.fileprovider"
|
||||
resValue "string", "linphone_address_mime_type", "vnd.android.cursor.item/vnd." + getPackageName() + ".provider.sip_address"
|
||||
|
||||
if (!firebaseEnabled()) {
|
||||
|
@ -165,35 +121,44 @@ android {
|
|||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.excludes = excludeFiles
|
||||
dataBinding {
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
excludes = excludePackage
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
maven {
|
||||
url file(LinphoneSdkBuildDir + '/maven_repository/')
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst 'META-INF/NOTICE'
|
||||
pickFirst 'META-INF/LICENSE'
|
||||
exclude 'META-INF/MANIFEST.MF'
|
||||
}
|
||||
/*maven {
|
||||
url "https://linphone.org/maven_repository"
|
||||
}*/
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly 'org.jetbrains:annotations:19.0.0'
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation "androidx.media:media:1.1.0"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.3'
|
||||
implementation 'androidx.core:core-ktx:1.2.0'
|
||||
implementation 'androidx.navigation:navigation-fragment-ktx:2.2.1'
|
||||
implementation 'androidx.navigation:navigation-ui-ktx:2.2.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.1.0'
|
||||
implementation 'com.google.android:flexbox:2.0.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.11.0'
|
||||
|
||||
if (firebaseEnabled()) {
|
||||
implementation 'com.google.firebase:firebase-messaging:19.0.1'
|
||||
}
|
||||
implementation 'androidx.media:media:1.2.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'com.google.android:flexbox:1.1.0'
|
||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||
implementation "org.linphone:linphone-sdk-android:4.5+"
|
||||
|
||||
implementation 'org.linphone:linphone-sdk-android:4.4+'
|
||||
}
|
||||
|
||||
if (firebaseEnabled()) {
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
}
|
||||
|
@ -210,12 +175,9 @@ task generateContactsXml(type: Copy) {
|
|||
}
|
||||
project.tasks['preBuild'].dependsOn 'generateContactsXml'
|
||||
|
||||
apply plugin: "com.diffplug.gradle.spotless"
|
||||
spotless {
|
||||
java {
|
||||
target '**/*.java'
|
||||
googleJavaFormat('1.6').aosp()
|
||||
removeUnusedImports()
|
||||
}
|
||||
ktlint {
|
||||
android = true
|
||||
ignoreFailures = true
|
||||
}
|
||||
project.tasks['preBuild'].dependsOn 'spotlessApply'
|
||||
|
||||
project.tasks['preBuild'].dependsOn 'ktlintFormat'
|
|
@ -4,7 +4,7 @@
|
|||
<ContactsDataKind
|
||||
android:detailColumn="data3"
|
||||
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:summaryColumn="data2" />
|
||||
<!-- You can't use @string/linphone_address_mime_type above ! You have to hardcode it... -->
|
||||
|
|
22
app/proguard-rules.pro
vendored
22
app/proguard-rules.pro
vendored
|
@ -1 +1,21 @@
|
|||
-dontwarn org.apache.**
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
|
|
245
app/src/main/AndroidManifest.xml
Executable file → Normal file
245
app/src/main/AndroidManifest.xml
Executable file → Normal file
|
@ -1,222 +1,122 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="org.linphone"
|
||||
android:installLocation="auto">
|
||||
package="org.linphone">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<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" />
|
||||
<!-- 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
|
||||
android:anyDensity="true"
|
||||
android:largeScreens="true"
|
||||
android:normalScreens="true"
|
||||
android:smallScreens="true"
|
||||
android:xlargeScreens="true" />
|
||||
<!-- Helps filling phone number and country code in assistant -->
|
||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
||||
|
||||
<!-- Needed for auto start at boot and to ensure the service won't be killed by OS while in call -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- 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
|
||||
android:name=".LinphoneApplication"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:resizeableActivity="true"
|
||||
android:theme="@style/LinphoneStyle"
|
||||
android:extractNativeLibs="${extractNativeLibs}"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
|
||||
<!-- Starting activities -->
|
||||
android:theme="@style/AppTheme">
|
||||
|
||||
<activity
|
||||
android:name=".activities.LinphoneLauncherActivity"
|
||||
android:noHistory="true">
|
||||
android:name=".activities.launcher.LauncherActivity"
|
||||
android:noHistory="true"
|
||||
android:theme="@style/LauncherTheme">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</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>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
|
||||
<data android:mimeType="text/*" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="audio/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
<data android:mimeType="application/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.OPENABLE" />
|
||||
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".contacts.ContactsActivity"
|
||||
android:launchMode="singleTop">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="${linphone_address_mime_type}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".history.HistoryActivity"
|
||||
android:launchMode="singleTop">
|
||||
<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>
|
||||
</activity>
|
||||
|
||||
<!-- Call activities -->
|
||||
<activity android:name=".activities.assistant.AssistantActivity"
|
||||
android:windowSoftInputMode="adjustResize"/>
|
||||
|
||||
<activity
|
||||
android:name=".call.CallIncomingActivity"
|
||||
<activity android:name=".activities.call.CallActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:showWhenLocked="true"
|
||||
android:supportsPictureInPicture="true" />
|
||||
|
||||
<activity android:name=".activities.call.IncomingCallActivity"
|
||||
android:launchMode="singleTop"
|
||||
android:noHistory="true"
|
||||
android:showWhenLocked="true"
|
||||
android:turnScreenOn="true"/>
|
||||
android:turnScreenOn="true" />
|
||||
|
||||
<activity
|
||||
android:name=".call.CallOutgoingActivity"
|
||||
<activity android:name=".activities.call.OutgoingCallActivity"
|
||||
android:launchMode="singleTop"
|
||||
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"/>
|
||||
|
||||
android:noHistory="true" />
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
<service
|
||||
android:name=".service.LinphoneService"
|
||||
android:label="@string/service_name" />
|
||||
android:name=".core.CoreService"
|
||||
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
|
||||
android:name=".sync.SyncService"
|
||||
android:name=".contact.DummySyncService"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.SyncAdapter" />
|
||||
|
@ -224,13 +124,13 @@
|
|||
|
||||
<meta-data
|
||||
android:name="android.content.SyncAdapter"
|
||||
android:resource="@xml/syncadapter" />
|
||||
android:resource="@xml/sync_adapter" />
|
||||
<meta-data
|
||||
android:name="android.provider.CONTACTS_STRUCTURE"
|
||||
android:resource="@xml/contacts" />
|
||||
</service>
|
||||
|
||||
<service android:name=".sync.AuthenticationService">
|
||||
<service android:name=".contact.DummyAuthenticationService">
|
||||
<intent-filter>
|
||||
<action android:name="android.accounts.AccountAuthenticator" />
|
||||
</intent-filter>
|
||||
|
@ -240,21 +140,11 @@
|
|||
android:resource="@xml/authenticator" />
|
||||
</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 -->
|
||||
|
||||
<receiver android:name=".receivers.BootReceiver">
|
||||
<receiver android:name=".core.CorePushReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
||||
<action android:name="org.linphone.core.action.PUSH_RECEIVED"/>
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
@ -263,10 +153,10 @@
|
|||
android:enabled="true"
|
||||
android:exported="false" />
|
||||
|
||||
<receiver
|
||||
android:name=".receivers.AccountEnableReceiver">
|
||||
<receiver android:name=".core.BootReceiver">
|
||||
<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>
|
||||
</receiver>
|
||||
|
||||
|
@ -283,4 +173,5 @@
|
|||
</provider>
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
|
@ -21,12 +21,6 @@
|
|||
<entry name="stun_server" overwrite="true"></entry>
|
||||
<entry name="protocols" overwrite="true"></entry>
|
||||
</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">
|
||||
<entry name="domain" overwrite="true"></entry>
|
||||
<entry name="algorithm" overwrite="true">MD5</entry>
|
|
@ -1,3 +1,6 @@
|
|||
|
||||
## Start of default rc
|
||||
|
||||
[sip]
|
||||
contact="Linphone Android" <sip:linphone.android@unknown-host>
|
||||
use_info=0
|
||||
|
@ -35,3 +38,5 @@ history_max_size=100
|
|||
[in-app-purchase]
|
||||
server_url=https://subscribe.linphone.org:444/inapp.php
|
||||
purchasable_items_ids=test_account_subscription
|
||||
|
||||
## End of default rc
|
|
@ -1,7 +1,9 @@
|
|||
|
||||
#
|
||||
## Start of factory rc
|
||||
|
||||
#This file shall not contain path referencing package name, in order to be portable when app is renamed.
|
||||
#Paths to resources must be set from LinphoneManager, after creating LinphoneCore.
|
||||
|
||||
[net]
|
||||
mtu=1300
|
||||
force_ice_disablement=0
|
||||
|
@ -37,3 +39,5 @@ xmlrpc_url=https://subscribe.linphone.org:444/wizard.php
|
|||
|
||||
[lime]
|
||||
lime_update_threshold=-1
|
||||
|
||||
## End of factory rc
|
|
@ -1,144 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2012 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.vending.billing;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
/**
|
||||
* InAppBillingService is the service that provides in-app billing version 3 and beyond.
|
||||
* This service provides the following features:
|
||||
* 1. Provides a new API to get details of in-app items published for the app including
|
||||
* price, type, title and description.
|
||||
* 2. The purchase flow is synchronous and purchase information is available immediately
|
||||
* after it completes.
|
||||
* 3. Purchase information of in-app purchases is maintained within the Google Play system
|
||||
* till the purchase is consumed.
|
||||
* 4. An API to consume a purchase of an inapp item. All purchases of one-time
|
||||
* in-app items are consumable and thereafter can be purchased again.
|
||||
* 5. An API to get current purchases of the user immediately. This will not contain any
|
||||
* consumed purchases.
|
||||
*
|
||||
* All calls will give a response code with the following possible values
|
||||
* RESULT_OK = 0 - success
|
||||
* RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
|
||||
* RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
|
||||
* RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
|
||||
* RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
|
||||
* RESULT_ERROR = 6 - Fatal error during the API action
|
||||
* RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
|
||||
* RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
|
||||
*/
|
||||
interface IInAppBillingService {
|
||||
/**
|
||||
* Checks support for the requested billing API version, package and in-app type.
|
||||
* Minimum API version supported by this interface is 3.
|
||||
* @param apiVersion the billing version which the app is using
|
||||
* @param packageName the package name of the calling app
|
||||
* @param type type of the in-app item being purchased "inapp" for one-time purchases
|
||||
* and "subs" for subscription.
|
||||
* @return RESULT_OK(0) on success, corresponding result code on failures
|
||||
*/
|
||||
int isBillingSupported(int apiVersion, String packageName, String type);
|
||||
|
||||
/**
|
||||
* Provides details of a list of SKUs
|
||||
* Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
|
||||
* with a list JSON strings containing the productId, price, title and description.
|
||||
* This API can be called with a maximum of 20 SKUs.
|
||||
* @param apiVersion billing API version that the Third-party is using
|
||||
* @param packageName the package name of the calling app
|
||||
* @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "DETAILS_LIST" with a StringArrayList containing purchase information
|
||||
* in JSON format similar to:
|
||||
* '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
|
||||
* "title : "Example Title", "description" : "This is an example description" }'
|
||||
*/
|
||||
Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
|
||||
|
||||
/**
|
||||
* Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
|
||||
* the type, a unique purchase token and an optional developer payload.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param sku the SKU of the in-app item as published in the developer console
|
||||
* @param type the type of the in-app item ("inapp" for one-time purchases
|
||||
* and "subs" for subscription).
|
||||
* @param developerPayload optional argument to be sent back with the purchase information
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "BUY_INTENT" - PendingIntent to start the purchase flow
|
||||
*
|
||||
* The Pending intent should be launched with startIntentSenderForResult. When purchase flow
|
||||
* has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
|
||||
* If the purchase is successful, the result data will contain the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "INAPP_PURCHASE_DATA" - String in JSON format similar to
|
||||
* '{"orderId":"12999763169054705758.1371079406387615",
|
||||
* "packageName":"com.example.app",
|
||||
* "productId":"exampleSku",
|
||||
* "purchaseTime":1345678900000,
|
||||
* "purchaseToken" : "122333444455555",
|
||||
* "developerPayload":"example developer payload" }'
|
||||
* "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
|
||||
* was signed with the private key of the developer
|
||||
* TODO: change this to app-specific keys.
|
||||
*/
|
||||
Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
|
||||
String developerPayload);
|
||||
|
||||
/**
|
||||
* Returns the current SKUs owned by the user of the type and package name specified along with
|
||||
* purchase information and a signature of the data to be validated.
|
||||
* This will return all SKUs that have been purchased in V3 and managed items purchased using
|
||||
* V1 and V2 that have not been consumed.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param type the type of the in-app items being requested
|
||||
* ("inapp" for one-time purchases and "subs" for subscription).
|
||||
* @param continuationToken to be set as null for the first call, if the number of owned
|
||||
* skus are too many, a continuationToken is returned in the response bundle.
|
||||
* This method can be called again with the continuation token to get the next set of
|
||||
* owned skus.
|
||||
* @return Bundle containing the following key-value pairs
|
||||
* "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
|
||||
* failure as listed above.
|
||||
* "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
|
||||
* "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
|
||||
* "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
|
||||
* of the purchase information
|
||||
* "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
|
||||
* next set of in-app purchases. Only set if the
|
||||
* user has more owned skus than the current list.
|
||||
*/
|
||||
Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
|
||||
|
||||
/**
|
||||
* Consume the last purchase of the given SKU. This will result in this item being removed
|
||||
* from all subsequent responses to getPurchases() and allow re-purchase of this item.
|
||||
* @param apiVersion billing API version that the app is using
|
||||
* @param packageName package name of the calling app
|
||||
* @param purchaseToken token in the purchase information JSON that identifies the purchase
|
||||
* to be consumed
|
||||
* @return 0 if consumption succeeded. Appropriate error values for failures.
|
||||
*/
|
||||
int consumePurchase(int apiVersion, String packageName, String purchaseToken);
|
||||
}
|
55
app/src/main/java/org/linphone/LinphoneApplication.kt
Normal file
55
app/src/main/java/org/linphone/LinphoneApplication.kt
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone
|
||||
|
||||
import android.app.Application
|
||||
import org.linphone.core.CoreContext
|
||||
import org.linphone.core.CorePreferences
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.LogCollectionState
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class LinphoneApplication : Application() {
|
||||
companion object {
|
||||
lateinit var corePreferences: CorePreferences
|
||||
lateinit var coreContext: CoreContext
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
val appName = getString(R.string.app_name)
|
||||
android.util.Log.i("[$appName]", "Application is being created")
|
||||
|
||||
Factory.instance().setLogCollectionPath(applicationContext.filesDir.absolutePath)
|
||||
Factory.instance().enableLogCollection(LogCollectionState.Enabled)
|
||||
|
||||
corePreferences = CorePreferences(applicationContext)
|
||||
corePreferences.copyAssetsFromPackage()
|
||||
|
||||
val config = Factory.instance().createConfigWithFactory(corePreferences.configPath, corePreferences.factoryConfigPath)
|
||||
corePreferences.config = config
|
||||
|
||||
Factory.instance().setDebugMode(corePreferences.debugLogs, appName)
|
||||
|
||||
coreContext = CoreContext(applicationContext, config)
|
||||
coreContext.start()
|
||||
Log.i("[Application] Created")
|
||||
}
|
||||
}
|
|
@ -1,342 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone;
|
||||
|
||||
import static android.content.Intent.ACTION_MAIN;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.provider.ContactsContract;
|
||||
import java.util.ArrayList;
|
||||
import org.linphone.call.CallActivity;
|
||||
import org.linphone.call.CallIncomingActivity;
|
||||
import org.linphone.call.CallOutgoingActivity;
|
||||
import org.linphone.compatibility.Compatibility;
|
||||
import org.linphone.contacts.ContactsManager;
|
||||
import org.linphone.core.Call;
|
||||
import org.linphone.core.ConfiguringState;
|
||||
import org.linphone.core.Core;
|
||||
import org.linphone.core.CoreListenerStub;
|
||||
import org.linphone.core.Factory;
|
||||
import org.linphone.core.GlobalState;
|
||||
import org.linphone.core.LogLevel;
|
||||
import org.linphone.core.LoggingService;
|
||||
import org.linphone.core.LoggingServiceListener;
|
||||
import org.linphone.core.tools.Log;
|
||||
import org.linphone.mediastream.Version;
|
||||
import org.linphone.notifications.NotificationsManager;
|
||||
import org.linphone.service.LinphoneService;
|
||||
import org.linphone.settings.LinphonePreferences;
|
||||
import org.linphone.utils.DeviceUtils;
|
||||
import org.linphone.utils.LinphoneUtils;
|
||||
import org.linphone.utils.PushNotificationUtils;
|
||||
|
||||
public class LinphoneContext {
|
||||
private static LinphoneContext sInstance = null;
|
||||
|
||||
private Context mContext;
|
||||
|
||||
private final LoggingServiceListener mJavaLoggingService =
|
||||
new LoggingServiceListener() {
|
||||
@Override
|
||||
public void onLogMessageWritten(
|
||||
LoggingService logService, String domain, LogLevel lev, String message) {
|
||||
switch (lev) {
|
||||
case Debug:
|
||||
android.util.Log.d(domain, message);
|
||||
break;
|
||||
case Message:
|
||||
android.util.Log.i(domain, message);
|
||||
break;
|
||||
case Warning:
|
||||
android.util.Log.w(domain, message);
|
||||
break;
|
||||
case Error:
|
||||
android.util.Log.e(domain, message);
|
||||
break;
|
||||
case Fatal:
|
||||
default:
|
||||
android.util.Log.wtf(domain, message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
private CoreListenerStub mListener;
|
||||
private NotificationsManager mNotificationManager;
|
||||
private LinphoneManager mLinphoneManager;
|
||||
private ContactsManager mContactsManager;
|
||||
private final ArrayList<CoreStartedListener> mCoreStartedListeners;
|
||||
|
||||
public static boolean isReady() {
|
||||
return sInstance != null;
|
||||
}
|
||||
|
||||
public static LinphoneContext instance() {
|
||||
if (sInstance == null) {
|
||||
throw new RuntimeException("[Context] Linphone Context not available!");
|
||||
}
|
||||
return sInstance;
|
||||
}
|
||||
|
||||
public LinphoneContext(Context context) {
|
||||
mContext = context;
|
||||
mCoreStartedListeners = new ArrayList<>();
|
||||
|
||||
LinphonePreferences.instance().setContext(context);
|
||||
Factory.instance().setLogCollectionPath(context.getFilesDir().getAbsolutePath());
|
||||
boolean isDebugEnabled = LinphonePreferences.instance().isDebugEnabled();
|
||||
LinphoneUtils.configureLoggingService(isDebugEnabled, context.getString(R.string.app_name));
|
||||
|
||||
// Dump some debugging information to the logs
|
||||
dumpDeviceInformation();
|
||||
dumpLinphoneInformation();
|
||||
|
||||
sInstance = this;
|
||||
Log.i("[Context] Ready");
|
||||
|
||||
mListener =
|
||||
new CoreListenerStub() {
|
||||
@Override
|
||||
public void onGlobalStateChanged(Core core, GlobalState state, String message) {
|
||||
Log.i("[Context] Global state is [", state, "]");
|
||||
|
||||
if (state == GlobalState.On) {
|
||||
for (CoreStartedListener listener : mCoreStartedListeners) {
|
||||
listener.onCoreStarted();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfiguringStatus(
|
||||
Core core, ConfiguringState status, String message) {
|
||||
Log.i("[Context] Configuring state is [", status, "]");
|
||||
|
||||
if (status == ConfiguringState.Successful) {
|
||||
LinphonePreferences.instance()
|
||||
.setPushNotificationEnabled(
|
||||
LinphonePreferences.instance()
|
||||
.isPushNotificationEnabled());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallStateChanged(
|
||||
Core core, Call call, Call.State state, String message) {
|
||||
Log.i("[Context] Call state is [", state, "]");
|
||||
|
||||
if (mContext.getResources().getBoolean(R.bool.enable_call_notification)) {
|
||||
mNotificationManager.displayCallNotification(call);
|
||||
}
|
||||
|
||||
if (state == Call.State.IncomingReceived
|
||||
|| state == Call.State.IncomingEarlyMedia) {
|
||||
// Starting SDK 24 (Android 7.0) we rely on the fullscreen intent of the
|
||||
// call incoming notification
|
||||
if (Version.sdkStrictlyBelow(Version.API24_NOUGAT_70)) {
|
||||
if (!mLinphoneManager.getCallGsmON()) onIncomingReceived();
|
||||
}
|
||||
|
||||
// In case of push notification Service won't be started until here
|
||||
if (!LinphoneService.isReady()) {
|
||||
Log.i("[Context] Service not running, starting it");
|
||||
Intent intent = new Intent(ACTION_MAIN);
|
||||
intent.setClass(mContext, LinphoneService.class);
|
||||
mContext.startService(intent);
|
||||
}
|
||||
} else if (state == Call.State.OutgoingInit) {
|
||||
onOutgoingStarted();
|
||||
} else if (state == Call.State.Connected) {
|
||||
onCallStarted();
|
||||
} else if (state == Call.State.End
|
||||
|| state == Call.State.Released
|
||||
|| state == Call.State.Error) {
|
||||
if (LinphoneService.isReady()) {
|
||||
LinphoneService.instance().destroyOverlay();
|
||||
}
|
||||
|
||||
if (state == Call.State.Released
|
||||
&& call.getCallLog().getStatus() == Call.Status.Missed) {
|
||||
mNotificationManager.displayMissedCallNotification(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mLinphoneManager = new LinphoneManager(context);
|
||||
mNotificationManager = new NotificationsManager(context);
|
||||
|
||||
if (DeviceUtils.isAppUserRestricted(mContext)) {
|
||||
// See https://firebase.google.com/docs/cloud-messaging/android/receive#restricted
|
||||
Log.w(
|
||||
"[Context] Device has been restricted by user (Android 9+), push notifications won't work !");
|
||||
}
|
||||
|
||||
int bucket = DeviceUtils.getAppStandbyBucket(mContext);
|
||||
if (bucket > 0) {
|
||||
Log.w(
|
||||
"[Context] Device is in bucket "
|
||||
+ Compatibility.getAppStandbyBucketNameFromValue(bucket));
|
||||
}
|
||||
|
||||
if (!PushNotificationUtils.isAvailable(mContext)) {
|
||||
Log.w("[Context] Push notifications won't work !");
|
||||
}
|
||||
}
|
||||
|
||||
public void start(boolean isPush) {
|
||||
Log.i("[Context] Starting, push status is ", isPush);
|
||||
mLinphoneManager.startLibLinphone(isPush, mListener);
|
||||
|
||||
mNotificationManager.onCoreReady();
|
||||
|
||||
mContactsManager = new ContactsManager(mContext);
|
||||
if (!Version.sdkAboveOrEqual(Version.API26_O_80)
|
||||
|| (mContactsManager.hasReadContactsAccess())) {
|
||||
mContext.getContentResolver()
|
||||
.registerContentObserver(
|
||||
ContactsContract.Contacts.CONTENT_URI, true, mContactsManager);
|
||||
}
|
||||
if (mContactsManager.hasReadContactsAccess()) {
|
||||
mContactsManager.enableContactsAccess();
|
||||
}
|
||||
mContactsManager.initializeContactManager();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
Log.i("[Context] Destroying");
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
core.removeListener(mListener);
|
||||
core = null; // To allow the gc calls below to free the Core
|
||||
}
|
||||
|
||||
// Make sure our notification is gone.
|
||||
if (mNotificationManager != null) {
|
||||
mNotificationManager.destroy();
|
||||
}
|
||||
|
||||
if (mContactsManager != null) {
|
||||
mContactsManager.destroy();
|
||||
}
|
||||
|
||||
// Destroy the LinphoneManager second to last to ensure any getCore() call will work
|
||||
if (mLinphoneManager != null) {
|
||||
mLinphoneManager.destroy();
|
||||
}
|
||||
|
||||
// Wait for every other object to be destroyed to make LinphoneService.instance() invalid
|
||||
sInstance = null;
|
||||
|
||||
if (LinphonePreferences.instance().useJavaLogger()) {
|
||||
Factory.instance().getLoggingService().removeListener(mJavaLoggingService);
|
||||
}
|
||||
LinphonePreferences.instance().destroy();
|
||||
}
|
||||
|
||||
public void updateContext(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public Context getApplicationContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
/* Managers accessors */
|
||||
|
||||
public LoggingServiceListener getJavaLoggingService() {
|
||||
return mJavaLoggingService;
|
||||
}
|
||||
|
||||
public NotificationsManager getNotificationManager() {
|
||||
return mNotificationManager;
|
||||
}
|
||||
|
||||
public LinphoneManager getLinphoneManager() {
|
||||
return mLinphoneManager;
|
||||
}
|
||||
|
||||
public ContactsManager getContactsManager() {
|
||||
return mContactsManager;
|
||||
}
|
||||
|
||||
public void addCoreStartedListener(CoreStartedListener listener) {
|
||||
mCoreStartedListeners.add(listener);
|
||||
}
|
||||
|
||||
public void removeCoreStartedListener(CoreStartedListener listener) {
|
||||
mCoreStartedListeners.remove(listener);
|
||||
}
|
||||
|
||||
/* Log device related information */
|
||||
|
||||
private void dumpDeviceInformation() {
|
||||
Log.i("==== Phone information dump ====");
|
||||
Log.i("DISPLAY NAME=" + Compatibility.getDeviceName(mContext));
|
||||
Log.i("DEVICE=" + Build.DEVICE);
|
||||
Log.i("MODEL=" + Build.MODEL);
|
||||
Log.i("MANUFACTURER=" + Build.MANUFACTURER);
|
||||
Log.i("ANDROID SDK=" + Build.VERSION.SDK_INT);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("ABIs=");
|
||||
for (String abi : Version.getCpuAbis()) {
|
||||
sb.append(abi).append(", ");
|
||||
}
|
||||
Log.i(sb.substring(0, sb.length() - 2));
|
||||
}
|
||||
|
||||
private void dumpLinphoneInformation() {
|
||||
Log.i("==== Linphone information dump ====");
|
||||
Log.i("VERSION NAME=" + org.linphone.BuildConfig.VERSION_NAME);
|
||||
Log.i("VERSION CODE=" + org.linphone.BuildConfig.VERSION_CODE);
|
||||
Log.i("PACKAGE=" + org.linphone.BuildConfig.APPLICATION_ID);
|
||||
Log.i("BUILD TYPE=" + org.linphone.BuildConfig.BUILD_TYPE);
|
||||
Log.i("SDK VERSION=" + mContext.getString(R.string.linphone_sdk_version));
|
||||
Log.i("SDK BRANCH=" + mContext.getString(R.string.linphone_sdk_branch));
|
||||
}
|
||||
|
||||
/* Call activities */
|
||||
|
||||
private void onIncomingReceived() {
|
||||
Intent intent = new Intent(mContext, CallIncomingActivity.class);
|
||||
// This flag is required to start an Activity from a Service context
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
|
||||
private void onOutgoingStarted() {
|
||||
Intent intent = new Intent(mContext, CallOutgoingActivity.class);
|
||||
// This flag is required to start an Activity from a Service context
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
|
||||
private void onCallStarted() {
|
||||
Intent intent = new Intent(mContext, CallActivity.class);
|
||||
// This flag is required to start an Activity from a Service context
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
mContext.startActivity(intent);
|
||||
}
|
||||
|
||||
public interface CoreStartedListener {
|
||||
void onCoreStarted();
|
||||
}
|
||||
}
|
|
@ -1,635 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.PowerManager;
|
||||
import android.os.PowerManager.WakeLock;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import java.io.File;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
import org.linphone.call.AndroidAudioManager;
|
||||
import org.linphone.call.CallManager;
|
||||
import org.linphone.contacts.ContactsManager;
|
||||
import org.linphone.core.AccountCreator;
|
||||
import org.linphone.core.Call;
|
||||
import org.linphone.core.Call.State;
|
||||
import org.linphone.core.Core;
|
||||
import org.linphone.core.CoreListener;
|
||||
import org.linphone.core.CoreListenerStub;
|
||||
import org.linphone.core.Factory;
|
||||
import org.linphone.core.FriendList;
|
||||
import org.linphone.core.PresenceActivity;
|
||||
import org.linphone.core.PresenceBasicStatus;
|
||||
import org.linphone.core.PresenceModel;
|
||||
import org.linphone.core.ProxyConfig;
|
||||
import org.linphone.core.Reason;
|
||||
import org.linphone.core.Tunnel;
|
||||
import org.linphone.core.TunnelConfig;
|
||||
import org.linphone.core.tools.Log;
|
||||
import org.linphone.settings.LinphonePreferences;
|
||||
import org.linphone.utils.LinphoneUtils;
|
||||
import org.linphone.utils.MediaScanner;
|
||||
import org.linphone.utils.PushNotificationUtils;
|
||||
|
||||
/** Handles Linphone's Core lifecycle */
|
||||
public class LinphoneManager implements SensorEventListener {
|
||||
private final String mBasePath;
|
||||
private final String mRingSoundFile;
|
||||
private final String mCallLogDatabaseFile;
|
||||
private final String mFriendsDatabaseFile;
|
||||
private final String mUserCertsPath;
|
||||
|
||||
private final Context mContext;
|
||||
private AndroidAudioManager mAudioManager;
|
||||
private CallManager mCallManager;
|
||||
private final PowerManager mPowerManager;
|
||||
private final ConnectivityManager mConnectivityManager;
|
||||
private TelephonyManager mTelephonyManager;
|
||||
private PhoneStateListener mPhoneStateListener;
|
||||
private WakeLock mProximityWakelock;
|
||||
private final SensorManager mSensorManager;
|
||||
private final Sensor mProximity;
|
||||
private final MediaScanner mMediaScanner;
|
||||
private Timer mTimer;
|
||||
|
||||
private final LinphonePreferences mPrefs;
|
||||
private Core mCore;
|
||||
private CoreListenerStub mCoreListener;
|
||||
private AccountCreator mAccountCreator;
|
||||
|
||||
private boolean mExited;
|
||||
private boolean mCallGsmON;
|
||||
private boolean mProximitySensingEnabled;
|
||||
private boolean mHasLastCallSasBeenRejected;
|
||||
private Runnable mIterateRunnable;
|
||||
|
||||
public LinphoneManager(Context c) {
|
||||
mExited = false;
|
||||
mContext = c;
|
||||
mBasePath = c.getFilesDir().getAbsolutePath();
|
||||
mCallLogDatabaseFile = mBasePath + "/linphone-log-history.db";
|
||||
mFriendsDatabaseFile = mBasePath + "/linphone-friends.db";
|
||||
mRingSoundFile = mBasePath + "/share/sounds/linphone/rings/notes_of_the_optimistic.mkv";
|
||||
mUserCertsPath = mBasePath + "/user-certs";
|
||||
|
||||
mPrefs = LinphonePreferences.instance();
|
||||
mPowerManager = (PowerManager) c.getSystemService(Context.POWER_SERVICE);
|
||||
mConnectivityManager =
|
||||
(ConnectivityManager) c.getSystemService(Context.CONNECTIVITY_SERVICE);
|
||||
mSensorManager = (SensorManager) c.getSystemService(Context.SENSOR_SERVICE);
|
||||
mProximity = mSensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY);
|
||||
mTelephonyManager = (TelephonyManager) c.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
mPhoneStateListener =
|
||||
new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String phoneNumber) {
|
||||
switch (state) {
|
||||
case TelephonyManager.CALL_STATE_OFFHOOK:
|
||||
Log.i("[Manager] Phone state is off hook");
|
||||
setCallGsmON(true);
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_RINGING:
|
||||
Log.i("[Manager] Phone state is ringing");
|
||||
setCallGsmON(true);
|
||||
break;
|
||||
case TelephonyManager.CALL_STATE_IDLE:
|
||||
Log.i("[Manager] Phone state is idle");
|
||||
setCallGsmON(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Log.i("[Manager] Registering phone state listener");
|
||||
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
|
||||
mHasLastCallSasBeenRejected = false;
|
||||
mCallManager = new CallManager(c);
|
||||
|
||||
File f = new File(mUserCertsPath);
|
||||
if (!f.exists()) {
|
||||
if (!f.mkdir()) {
|
||||
Log.e("[Manager] " + mUserCertsPath + " can't be created.");
|
||||
}
|
||||
}
|
||||
|
||||
mMediaScanner = new MediaScanner(c);
|
||||
|
||||
mCoreListener =
|
||||
new CoreListenerStub() {
|
||||
@SuppressLint("Wakelock")
|
||||
@Override
|
||||
public void onCallStateChanged(
|
||||
final Core core,
|
||||
final Call call,
|
||||
final State state,
|
||||
final String message) {
|
||||
Log.i("[Manager] Call state is [", state, "]");
|
||||
if (state == State.IncomingReceived
|
||||
&& !call.equals(core.getCurrentCall())) {
|
||||
if (call.getReplacedCall() != null) {
|
||||
// attended transfer will be accepted automatically.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ((state == State.IncomingReceived || state == State.IncomingEarlyMedia)
|
||||
&& getCallGsmON()) {
|
||||
if (mCore != null) {
|
||||
call.decline(Reason.Busy);
|
||||
}
|
||||
} else if (state == State.IncomingReceived
|
||||
&& (LinphonePreferences.instance().isAutoAnswerEnabled())
|
||||
&& !getCallGsmON()) {
|
||||
LinphoneUtils.dispatchOnUIThreadAfter(
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mCore != null) {
|
||||
if (mCore.getCallsNb() > 0) {
|
||||
mCallManager.acceptCall(call);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mPrefs.getAutoAnswerTime());
|
||||
} else if (state == State.End || state == State.Error) {
|
||||
if (mCore.getCallsNb() == 0) {
|
||||
// Disabling proximity sensor
|
||||
enableProximitySensing(false);
|
||||
}
|
||||
} else if (state == State.UpdatedByRemote) {
|
||||
// If the correspondent proposes video while audio call
|
||||
boolean remoteVideo = call.getRemoteParams().videoEnabled();
|
||||
boolean localVideo = call.getCurrentParams().videoEnabled();
|
||||
boolean autoAcceptCameraPolicy =
|
||||
LinphonePreferences.instance()
|
||||
.shouldAutomaticallyAcceptVideoRequests();
|
||||
if (remoteVideo
|
||||
&& !localVideo
|
||||
&& !autoAcceptCameraPolicy
|
||||
&& mCore.getConference() == null) {
|
||||
call.deferUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFriendListCreated(Core core, FriendList list) {
|
||||
if (LinphoneContext.isReady()) {
|
||||
list.addListener(ContactsManager.getInstance());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFriendListRemoved(Core core, FriendList list) {
|
||||
list.removeListener(ContactsManager.getInstance());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static synchronized LinphoneManager getInstance() {
|
||||
LinphoneManager manager = LinphoneContext.instance().getLinphoneManager();
|
||||
if (manager == null) {
|
||||
throw new RuntimeException(
|
||||
"[Manager] Linphone Manager should be created before accessed");
|
||||
}
|
||||
if (manager.mExited) {
|
||||
throw new RuntimeException(
|
||||
"[Manager] Linphone Manager was already destroyed. "
|
||||
+ "Better use getCore and check returned value");
|
||||
}
|
||||
return manager;
|
||||
}
|
||||
|
||||
public static synchronized AndroidAudioManager getAudioManager() {
|
||||
return getInstance().mAudioManager;
|
||||
}
|
||||
|
||||
public static synchronized CallManager getCallManager() {
|
||||
return getInstance().mCallManager;
|
||||
}
|
||||
|
||||
public static synchronized Core getCore() {
|
||||
if (!LinphoneContext.isReady()) return null;
|
||||
|
||||
if (getInstance().mExited) {
|
||||
// Can occur if the UI thread play a posted event but in the meantime the
|
||||
// LinphoneManager was destroyed
|
||||
// Ex: stop call and quickly terminate application.
|
||||
return null;
|
||||
}
|
||||
return getInstance().mCore;
|
||||
}
|
||||
|
||||
/* End of static */
|
||||
|
||||
public MediaScanner getMediaScanner() {
|
||||
return mMediaScanner;
|
||||
}
|
||||
|
||||
public synchronized void destroy() {
|
||||
destroyManager();
|
||||
// Wait for Manager to destroy everything before setting mExited to true
|
||||
// Otherwise some objects might crash during their own destroy if they try to call
|
||||
// LinphoneManager.getCore(), for example to unregister a listener
|
||||
mExited = true;
|
||||
}
|
||||
|
||||
public void restartCore() {
|
||||
Log.w("[Manager] Restarting Core");
|
||||
mCore.stop();
|
||||
mCore.start();
|
||||
}
|
||||
|
||||
private void destroyCore() {
|
||||
Log.w("[Manager] Destroying Core");
|
||||
mCore.stop();
|
||||
mCore.removeListener(mCoreListener);
|
||||
}
|
||||
|
||||
private synchronized void destroyManager() {
|
||||
Log.w("[Manager] Destroying Manager");
|
||||
changeStatusToOffline();
|
||||
|
||||
if (mTelephonyManager != null) {
|
||||
Log.i("[Manager] Unregistering phone state listener");
|
||||
mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
}
|
||||
|
||||
if (mCallManager != null) mCallManager.destroy();
|
||||
if (mMediaScanner != null) mMediaScanner.destroy();
|
||||
if (mAudioManager != null) mAudioManager.destroy();
|
||||
|
||||
if (mTimer != null) mTimer.cancel();
|
||||
|
||||
if (mCore != null) {
|
||||
destroyCore();
|
||||
mCore = null;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void startLibLinphone(boolean isPush, CoreListener listener) {
|
||||
try {
|
||||
mCore =
|
||||
Factory.instance()
|
||||
.createCore(
|
||||
mPrefs.getLinphoneDefaultConfig(),
|
||||
mPrefs.getLinphoneFactoryConfig(),
|
||||
mContext);
|
||||
mCore.addListener(listener);
|
||||
mCore.addListener(mCoreListener);
|
||||
|
||||
if (isPush) {
|
||||
Log.w(
|
||||
"[Manager] We are here because of a received push notification, enter background mode before starting the Core");
|
||||
mCore.enterBackground();
|
||||
}
|
||||
|
||||
mCore.start();
|
||||
|
||||
mIterateRunnable =
|
||||
new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (mCore != null) {
|
||||
mCore.iterate();
|
||||
}
|
||||
}
|
||||
};
|
||||
TimerTask lTask =
|
||||
new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
LinphoneUtils.dispatchOnUIThread(mIterateRunnable);
|
||||
}
|
||||
};
|
||||
/*use schedule instead of scheduleAtFixedRate to avoid iterate from being call in burst after cpu wake up*/
|
||||
mTimer = new Timer("Linphone scheduler");
|
||||
mTimer.schedule(lTask, 0, 20);
|
||||
|
||||
configureCore();
|
||||
} catch (Exception e) {
|
||||
Log.e(e, "[Manager] Cannot start linphone");
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void configureCore() {
|
||||
Log.i("[Manager] Configuring Core");
|
||||
mAudioManager = new AndroidAudioManager(mContext);
|
||||
|
||||
mCore.setZrtpSecretsFile(mBasePath + "/zrtp_secrets");
|
||||
|
||||
String deviceName = mPrefs.getDeviceName(mContext);
|
||||
String appName = mContext.getResources().getString(R.string.user_agent);
|
||||
String androidVersion = org.linphone.BuildConfig.VERSION_NAME;
|
||||
String userAgent = appName + "/" + androidVersion + " (" + deviceName + ") LinphoneSDK";
|
||||
|
||||
mCore.setUserAgent(
|
||||
userAgent,
|
||||
getString(R.string.linphone_sdk_version)
|
||||
+ " ("
|
||||
+ getString(R.string.linphone_sdk_branch)
|
||||
+ ")");
|
||||
|
||||
// mCore.setChatDatabasePath(mChatDatabaseFile);
|
||||
mCore.setCallLogsDatabasePath(mCallLogDatabaseFile);
|
||||
mCore.setFriendsDatabasePath(mFriendsDatabaseFile);
|
||||
mCore.setUserCertificatesPath(mUserCertsPath);
|
||||
// mCore.setCallErrorTone(Reason.NotFound, mErrorToneFile);
|
||||
enableDeviceRingtone(mPrefs.isDeviceRingtoneEnabled());
|
||||
|
||||
int availableCores = Runtime.getRuntime().availableProcessors();
|
||||
Log.w("[Manager] MediaStreamer : " + availableCores + " cores detected and configured");
|
||||
|
||||
mCore.migrateLogsFromRcToDb();
|
||||
|
||||
// Migrate existing linphone accounts to have conference factory uri and LIME X3Dh url set
|
||||
String uri = getString(R.string.default_conference_factory_uri);
|
||||
for (ProxyConfig lpc : mCore.getProxyConfigList()) {
|
||||
if (lpc.getIdentityAddress().getDomain().equals(getString(R.string.default_domain))) {
|
||||
if (lpc.getConferenceFactoryUri() == null) {
|
||||
lpc.edit();
|
||||
Log.i(
|
||||
"[Manager] Setting conference factory on proxy config "
|
||||
+ lpc.getIdentityAddress().asString()
|
||||
+ " to default value: "
|
||||
+ uri);
|
||||
lpc.setConferenceFactoryUri(uri);
|
||||
lpc.done();
|
||||
}
|
||||
|
||||
if (mCore.limeX3DhAvailable()) {
|
||||
String url = mCore.getLimeX3DhServerUrl();
|
||||
if (url == null || url.isEmpty()) {
|
||||
url = getString(R.string.default_lime_x3dh_server_url);
|
||||
Log.i("[Manager] Setting LIME X3Dh server url to default value: " + url);
|
||||
mCore.setLimeX3DhServerUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mContext.getResources().getBoolean(R.bool.enable_push_id)) {
|
||||
PushNotificationUtils.init(mContext);
|
||||
}
|
||||
|
||||
mProximityWakelock =
|
||||
mPowerManager.newWakeLock(
|
||||
PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK,
|
||||
mContext.getPackageName() + ";manager_proximity_sensor");
|
||||
|
||||
resetCameraFromPreferences();
|
||||
|
||||
mAccountCreator = mCore.createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl());
|
||||
mCallGsmON = false;
|
||||
|
||||
Log.i("[Manager] Core configured");
|
||||
}
|
||||
|
||||
public void resetCameraFromPreferences() {
|
||||
Core core = getCore();
|
||||
if (core == null) return;
|
||||
|
||||
boolean useFrontCam = LinphonePreferences.instance().useFrontCam();
|
||||
String firstDevice = null;
|
||||
for (String camera : core.getVideoDevicesList()) {
|
||||
if (firstDevice == null) {
|
||||
firstDevice = camera;
|
||||
}
|
||||
|
||||
if (useFrontCam) {
|
||||
if (camera.contains("Front")) {
|
||||
Log.i("[Manager] Found front facing camera: " + camera);
|
||||
core.setVideoDevice(camera);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.i("[Manager] Using first camera available: " + firstDevice);
|
||||
core.setVideoDevice(firstDevice);
|
||||
}
|
||||
|
||||
/* Account linking */
|
||||
|
||||
public AccountCreator getAccountCreator() {
|
||||
if (mAccountCreator == null) {
|
||||
Log.w("[Manager] Account creator shouldn't be null !");
|
||||
mAccountCreator =
|
||||
mCore.createAccountCreator(LinphonePreferences.instance().getXmlrpcUrl());
|
||||
}
|
||||
return mAccountCreator;
|
||||
}
|
||||
|
||||
/* Presence stuff */
|
||||
|
||||
private boolean isPresenceModelActivitySet() {
|
||||
if (mCore != null) {
|
||||
return mCore.getPresenceModel() != null
|
||||
&& mCore.getPresenceModel().getActivity() != null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void changeStatusToOnline() {
|
||||
if (mCore == null) return;
|
||||
PresenceModel model = mCore.createPresenceModel();
|
||||
model.setBasicStatus(PresenceBasicStatus.Open);
|
||||
mCore.setPresenceModel(model);
|
||||
}
|
||||
|
||||
public void changeStatusToOnThePhone() {
|
||||
if (mCore == null) return;
|
||||
|
||||
if (isPresenceModelActivitySet()
|
||||
&& mCore.getPresenceModel().getActivity().getType()
|
||||
!= PresenceActivity.Type.OnThePhone) {
|
||||
mCore.getPresenceModel().getActivity().setType(PresenceActivity.Type.OnThePhone);
|
||||
} else if (!isPresenceModelActivitySet()) {
|
||||
PresenceModel model =
|
||||
mCore.createPresenceModelWithActivity(PresenceActivity.Type.OnThePhone, null);
|
||||
mCore.setPresenceModel(model);
|
||||
}
|
||||
}
|
||||
|
||||
private void changeStatusToOffline() {
|
||||
if (mCore != null) {
|
||||
PresenceModel model = mCore.getPresenceModel();
|
||||
model.setBasicStatus(PresenceBasicStatus.Closed);
|
||||
mCore.setPresenceModel(model);
|
||||
}
|
||||
}
|
||||
|
||||
/* Tunnel stuff */
|
||||
|
||||
public void initTunnelFromConf() {
|
||||
if (!mCore.tunnelAvailable()) return;
|
||||
|
||||
NetworkInfo info = mConnectivityManager.getActiveNetworkInfo();
|
||||
Tunnel tunnel = mCore.getTunnel();
|
||||
tunnel.cleanServers();
|
||||
TunnelConfig config = mPrefs.getTunnelConfig();
|
||||
if (config.getHost() != null) {
|
||||
tunnel.addServer(config);
|
||||
manageTunnelServer(info);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTunnelNeeded(NetworkInfo info) {
|
||||
if (info == null) {
|
||||
Log.i("[Manager] No connectivity: tunnel should be disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
String pref = mPrefs.getTunnelMode();
|
||||
|
||||
if (getString(R.string.tunnel_mode_entry_value_always).equals(pref)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (info.getType() != ConnectivityManager.TYPE_WIFI
|
||||
&& getString(R.string.tunnel_mode_entry_value_3G_only).equals(pref)) {
|
||||
Log.i("[Manager] Need tunnel: 'no wifi' connection");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void manageTunnelServer(NetworkInfo info) {
|
||||
if (mCore == null) return;
|
||||
if (!mCore.tunnelAvailable()) return;
|
||||
Tunnel tunnel = mCore.getTunnel();
|
||||
|
||||
Log.i("[Manager] Managing tunnel");
|
||||
if (isTunnelNeeded(info)) {
|
||||
Log.i("[Manager] Tunnel need to be activated");
|
||||
tunnel.setMode(Tunnel.Mode.Enable);
|
||||
} else {
|
||||
Log.i("[Manager] Tunnel should not be used");
|
||||
String pref = mPrefs.getTunnelMode();
|
||||
tunnel.setMode(Tunnel.Mode.Disable);
|
||||
if (getString(R.string.tunnel_mode_entry_value_auto).equals(pref)) {
|
||||
tunnel.setMode(Tunnel.Mode.Auto);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Proximity sensor stuff */
|
||||
|
||||
public void enableProximitySensing(boolean enable) {
|
||||
if (enable) {
|
||||
if (!mProximitySensingEnabled) {
|
||||
mSensorManager.registerListener(
|
||||
this, mProximity, SensorManager.SENSOR_DELAY_NORMAL);
|
||||
mProximitySensingEnabled = true;
|
||||
}
|
||||
} else {
|
||||
if (mProximitySensingEnabled) {
|
||||
mSensorManager.unregisterListener(this);
|
||||
mProximitySensingEnabled = false;
|
||||
// Don't forgeting to release wakelock if held
|
||||
if (mProximityWakelock.isHeld()) {
|
||||
mProximityWakelock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean isProximitySensorNearby(final SensorEvent event) {
|
||||
float threshold = 4.001f; // <= 4 cm is near
|
||||
|
||||
final float distanceInCm = event.values[0];
|
||||
final float maxDistance = event.sensor.getMaximumRange();
|
||||
Log.d(
|
||||
"[Manager] Proximity sensor report ["
|
||||
+ distanceInCm
|
||||
+ "] , for max range ["
|
||||
+ maxDistance
|
||||
+ "]");
|
||||
|
||||
if (maxDistance <= threshold) {
|
||||
// Case binary 0/1 and short sensors
|
||||
threshold = maxDistance;
|
||||
}
|
||||
return distanceInCm < threshold;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.timestamp == 0) return;
|
||||
if (isProximitySensorNearby(event)) {
|
||||
if (!mProximityWakelock.isHeld()) {
|
||||
mProximityWakelock.acquire();
|
||||
}
|
||||
} else {
|
||||
if (mProximityWakelock.isHeld()) {
|
||||
mProximityWakelock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {}
|
||||
|
||||
/* Other stuff */
|
||||
|
||||
public void enableDeviceRingtone(boolean use) {
|
||||
if (use) {
|
||||
mCore.setRing(null);
|
||||
} else {
|
||||
mCore.setRing(mRingSoundFile);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getCallGsmON() {
|
||||
return mCallGsmON;
|
||||
}
|
||||
|
||||
public void setCallGsmON(boolean on) {
|
||||
mCallGsmON = on;
|
||||
if (on && mCore != null) {
|
||||
mCore.pauseAllCalls();
|
||||
}
|
||||
}
|
||||
|
||||
private String getString(int key) {
|
||||
return mContext.getString(key);
|
||||
}
|
||||
|
||||
public boolean hasLastCallSasBeenRejected() {
|
||||
return mHasLastCallSasBeenRejected;
|
||||
}
|
||||
|
||||
public void lastCallSasRejected(boolean rejected) {
|
||||
mHasLastCallSasBeenRejected = rejected;
|
||||
}
|
||||
}
|
|
@ -1,209 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import org.linphone.LinphoneManager;
|
||||
import org.linphone.R;
|
||||
import org.linphone.core.Core;
|
||||
import org.linphone.core.CoreListenerStub;
|
||||
import org.linphone.settings.LinphonePreferences;
|
||||
|
||||
public class AboutActivity extends MainActivity {
|
||||
private CoreListenerStub mListener;
|
||||
private ProgressDialog mProgress;
|
||||
private boolean mUploadInProgress;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
mOnBackPressGoHome = false;
|
||||
mAlwaysHideTabBar = true;
|
||||
|
||||
// Uses the fragment container layout to inflate the about view instead of using a fragment
|
||||
View aboutView = LayoutInflater.from(this).inflate(R.layout.about, null, false);
|
||||
LinearLayout fragmentContainer = findViewById(R.id.fragmentContainer);
|
||||
LinearLayout.LayoutParams params =
|
||||
new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
fragmentContainer.addView(aboutView, params);
|
||||
|
||||
if (isTablet()) {
|
||||
findViewById(R.id.fragmentContainer2).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
TextView aboutVersion = findViewById(R.id.about_android_version);
|
||||
TextView aboutLiblinphoneVersion = findViewById(R.id.about_liblinphone_sdk_version);
|
||||
aboutLiblinphoneVersion.setText(
|
||||
String.format(
|
||||
getString(R.string.about_liblinphone_sdk_version),
|
||||
getString(R.string.linphone_sdk_version)
|
||||
+ " ("
|
||||
+ getString(R.string.linphone_sdk_branch)
|
||||
+ ")"));
|
||||
// We can't access a library's BuildConfig, so we have to set it as a resource
|
||||
aboutVersion.setText(
|
||||
String.format(
|
||||
getString(R.string.about_version),
|
||||
org.linphone.BuildConfig.VERSION_NAME
|
||||
+ " ("
|
||||
+ org.linphone.BuildConfig.BUILD_TYPE
|
||||
+ ")"));
|
||||
|
||||
TextView privacyPolicy = findViewById(R.id.privacy_policy_link);
|
||||
privacyPolicy.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent browserIntent =
|
||||
new Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_privacy_policy_link)));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
});
|
||||
|
||||
TextView license = findViewById(R.id.about_text);
|
||||
license.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent browserIntent =
|
||||
new Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_license_link)));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
});
|
||||
|
||||
Button sendLogs = findViewById(R.id.send_log);
|
||||
sendLogs.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
core.uploadLogCollection();
|
||||
}
|
||||
}
|
||||
});
|
||||
sendLogs.setVisibility(
|
||||
LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE);
|
||||
|
||||
Button resetLogs = findViewById(R.id.reset_log);
|
||||
resetLogs.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
core.resetLogCollection();
|
||||
}
|
||||
}
|
||||
});
|
||||
resetLogs.setVisibility(
|
||||
LinphonePreferences.instance().isDebugEnabled() ? View.VISIBLE : View.GONE);
|
||||
|
||||
mListener =
|
||||
new CoreListenerStub() {
|
||||
@Override
|
||||
public void onLogCollectionUploadProgressIndication(
|
||||
Core core, int offset, int total) {}
|
||||
|
||||
@Override
|
||||
public void onLogCollectionUploadStateChanged(
|
||||
Core core, Core.LogCollectionUploadState state, String info) {
|
||||
if (state == Core.LogCollectionUploadState.InProgress) {
|
||||
displayUploadLogsInProgress();
|
||||
} else if (state == Core.LogCollectionUploadState.Delivered
|
||||
|| state == Core.LogCollectionUploadState.NotDelivered) {
|
||||
mUploadInProgress = false;
|
||||
if (mProgress != null) mProgress.dismiss();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
showTopBarWithTitle(getString(R.string.about));
|
||||
if (getResources().getBoolean(R.bool.hide_bottom_bar_on_second_level_views)) {
|
||||
hideTabBar();
|
||||
}
|
||||
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
core.addListener(mListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
core.removeListener(mListener);
|
||||
}
|
||||
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
mListener = null;
|
||||
mProgress = null;
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void displayUploadLogsInProgress() {
|
||||
if (mUploadInProgress) {
|
||||
return;
|
||||
}
|
||||
mUploadInProgress = true;
|
||||
|
||||
mProgress = ProgressDialog.show(this, null, null);
|
||||
Drawable d = new ColorDrawable(ContextCompat.getColor(this, R.color.light_grey_color));
|
||||
d.setAlpha(200);
|
||||
mProgress
|
||||
.getWindow()
|
||||
.setLayout(
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.MATCH_PARENT);
|
||||
mProgress.getWindow().setBackgroundDrawable(d);
|
||||
mProgress.setContentView(R.layout.wait_layout);
|
||||
mProgress.show();
|
||||
}
|
||||
}
|
83
app/src/main/java/org/linphone/activities/GenericActivity.kt
Normal file
83
app/src/main/java/org/linphone/activities/GenericActivity.kt
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.Surface
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
abstract class GenericActivity : AppCompatActivity() {
|
||||
@SuppressLint("SourceLockedOrientationActivity")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
if (corePreferences.forcePortrait) {
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
|
||||
val nightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
val darkModeEnabled = corePreferences.darkMode
|
||||
when (nightMode) {
|
||||
Configuration.UI_MODE_NIGHT_NO, Configuration.UI_MODE_NIGHT_UNDEFINED -> {
|
||||
if (darkModeEnabled == 1) {
|
||||
// Force dark mode
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
}
|
||||
Configuration.UI_MODE_NIGHT_YES -> {
|
||||
if (darkModeEnabled == 0) {
|
||||
// Force light mode
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
var degrees = 270
|
||||
val orientation = windowManager.defaultDisplay.rotation
|
||||
when (orientation) {
|
||||
Surface.ROTATION_0 -> degrees = 0
|
||||
Surface.ROTATION_90 -> degrees = 270
|
||||
Surface.ROTATION_180 -> degrees = 180
|
||||
Surface.ROTATION_270 -> degrees = 90
|
||||
}
|
||||
Log.i("[Generic Activity] Device orientation is $degrees (raw value is $orientation)")
|
||||
val rotation = (360 - degrees) % 360
|
||||
coreContext.core.deviceRotation = rotation
|
||||
|
||||
// Remove service notification if it has been started by device boot
|
||||
coreContext.notificationsManager.stopForegroundNotificationIfPossible()
|
||||
}
|
||||
|
||||
fun isTablet(): Boolean {
|
||||
return resources.getBoolean(R.bool.isTablet)
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.Surface;
|
||||
import org.linphone.LinphoneContext;
|
||||
import org.linphone.LinphoneManager;
|
||||
import org.linphone.core.Core;
|
||||
import org.linphone.core.tools.Log;
|
||||
import org.linphone.service.LinphoneService;
|
||||
|
||||
public abstract class LinphoneGenericActivity extends ThemeableActivity {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
ensureServiceIsRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
ensureServiceIsRunning();
|
||||
|
||||
if (LinphoneContext.isReady()) {
|
||||
int degrees = 270;
|
||||
int orientation = getWindowManager().getDefaultDisplay().getRotation();
|
||||
switch (orientation) {
|
||||
case Surface.ROTATION_0:
|
||||
degrees = 0;
|
||||
break;
|
||||
case Surface.ROTATION_90:
|
||||
degrees = 270;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
degrees = 180;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
degrees = 90;
|
||||
break;
|
||||
}
|
||||
|
||||
Log.i(
|
||||
"[Generic Activity] Device orientation is "
|
||||
+ degrees
|
||||
+ " (raw value is "
|
||||
+ orientation
|
||||
+ ")");
|
||||
|
||||
int rotation = (360 - degrees) % 360;
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
core.setDeviceRotation(rotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureServiceIsRunning() {
|
||||
if (!LinphoneService.isReady()) {
|
||||
if (!LinphoneContext.isReady()) {
|
||||
new LinphoneContext(getApplicationContext());
|
||||
LinphoneContext.instance().start(false);
|
||||
Log.i("[Generic Activity] Context created & started");
|
||||
}
|
||||
|
||||
Log.i("[Generic Activity] Starting Service");
|
||||
try {
|
||||
startService(new Intent().setClass(this, LinphoneService.class));
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.e("[Generic Activity] Couldn't start service, exception: ", ise);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import org.linphone.LinphoneManager;
|
||||
import org.linphone.R;
|
||||
import org.linphone.assistant.MenuAssistantActivity;
|
||||
import org.linphone.chat.ChatActivity;
|
||||
import org.linphone.contacts.ContactsActivity;
|
||||
import org.linphone.dialer.DialerActivity;
|
||||
import org.linphone.history.HistoryActivity;
|
||||
import org.linphone.service.LinphoneService;
|
||||
import org.linphone.service.ServiceWaitThread;
|
||||
import org.linphone.service.ServiceWaitThreadListener;
|
||||
import org.linphone.settings.LinphonePreferences;
|
||||
|
||||
/** Creates LinphoneService and wait until Core is ready to start main Activity */
|
||||
public class LinphoneLauncherActivity extends Activity implements ServiceWaitThreadListener {
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (getResources().getBoolean(R.bool.orientation_portrait_only)) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
|
||||
if (!getResources().getBoolean(R.bool.use_full_screen_image_splashscreen)) {
|
||||
setContentView(R.layout.launch_screen);
|
||||
} // Otherwise use drawable/launch_screen layer list up until first activity starts
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
if (LinphoneService.isReady()) {
|
||||
onServiceReady();
|
||||
} else {
|
||||
try {
|
||||
startService(
|
||||
new Intent()
|
||||
.setClass(LinphoneLauncherActivity.this, LinphoneService.class));
|
||||
new ServiceWaitThread(this).start();
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.e("Linphone", "Exception raised while starting service: " + ise);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceReady() {
|
||||
final Class<? extends Activity> classToStart;
|
||||
|
||||
boolean useFirstLoginActivity =
|
||||
getResources().getBoolean(R.bool.display_account_assistant_at_first_start);
|
||||
if (useFirstLoginActivity && LinphonePreferences.instance().isFirstLaunch()) {
|
||||
classToStart = MenuAssistantActivity.class;
|
||||
} else {
|
||||
if (getIntent().getExtras() != null) {
|
||||
String activity = getIntent().getExtras().getString("Activity", null);
|
||||
if (ChatActivity.NAME.equals(activity)) {
|
||||
classToStart = ChatActivity.class;
|
||||
} else if (HistoryActivity.NAME.equals(activity)) {
|
||||
classToStart = HistoryActivity.class;
|
||||
} else if (ContactsActivity.NAME.equals(activity)) {
|
||||
classToStart = ContactsActivity.class;
|
||||
} else {
|
||||
classToStart = DialerActivity.class;
|
||||
}
|
||||
} else {
|
||||
classToStart = DialerActivity.class;
|
||||
}
|
||||
}
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setClass(LinphoneLauncherActivity.this, classToStart);
|
||||
if (getIntent() != null && getIntent().getExtras() != null) {
|
||||
intent.putExtras(getIntent().getExtras());
|
||||
}
|
||||
intent.setAction(getIntent().getAction());
|
||||
intent.setType(getIntent().getType());
|
||||
intent.setData(getIntent().getData());
|
||||
startActivity(intent);
|
||||
|
||||
LinphoneManager.getInstance().changeStatusToOnline();
|
||||
}
|
||||
}
|
|
@ -1,968 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Dialog;
|
||||
import android.app.Fragment;
|
||||
import android.app.FragmentManager;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import org.linphone.LinphoneContext;
|
||||
import org.linphone.LinphoneManager;
|
||||
import org.linphone.R;
|
||||
import org.linphone.assistant.PhoneAccountLinkingAssistantActivity;
|
||||
import org.linphone.call.CallActivity;
|
||||
import org.linphone.call.CallIncomingActivity;
|
||||
import org.linphone.call.CallOutgoingActivity;
|
||||
import org.linphone.chat.ChatActivity;
|
||||
import org.linphone.compatibility.Compatibility;
|
||||
import org.linphone.contacts.ContactsActivity;
|
||||
import org.linphone.contacts.ContactsManager;
|
||||
import org.linphone.contacts.LinphoneContact;
|
||||
import org.linphone.core.AccountCreator;
|
||||
import org.linphone.core.AccountCreatorListenerStub;
|
||||
import org.linphone.core.Address;
|
||||
import org.linphone.core.Call;
|
||||
import org.linphone.core.ChatMessage;
|
||||
import org.linphone.core.ChatRoom;
|
||||
import org.linphone.core.Core;
|
||||
import org.linphone.core.CoreListenerStub;
|
||||
import org.linphone.core.ProxyConfig;
|
||||
import org.linphone.core.RegistrationState;
|
||||
import org.linphone.core.tools.Log;
|
||||
import org.linphone.dialer.DialerActivity;
|
||||
import org.linphone.fragments.EmptyFragment;
|
||||
import org.linphone.fragments.StatusBarFragment;
|
||||
import org.linphone.history.HistoryActivity;
|
||||
import org.linphone.menu.SideMenuFragment;
|
||||
import org.linphone.service.LinphoneService;
|
||||
import org.linphone.settings.LinphonePreferences;
|
||||
import org.linphone.settings.SettingsActivity;
|
||||
import org.linphone.utils.DeviceUtils;
|
||||
import org.linphone.utils.LinphoneUtils;
|
||||
|
||||
public abstract class MainActivity extends LinphoneGenericActivity
|
||||
implements StatusBarFragment.MenuClikedListener, SideMenuFragment.QuitClikedListener {
|
||||
private static final int MAIN_PERMISSIONS = 1;
|
||||
protected static final int FRAGMENT_SPECIFIC_PERMISSION = 2;
|
||||
|
||||
private TextView mMissedCalls;
|
||||
private TextView mMissedMessages;
|
||||
protected View mContactsSelected;
|
||||
protected View mHistorySelected;
|
||||
protected View mDialerSelected;
|
||||
protected View mChatSelected;
|
||||
private LinearLayout mTopBar;
|
||||
private TextView mTopBarTitle;
|
||||
private LinearLayout mTabBar;
|
||||
|
||||
private SideMenuFragment mSideMenuFragment;
|
||||
private StatusBarFragment mStatusBarFragment;
|
||||
|
||||
protected boolean mOnBackPressGoHome;
|
||||
protected boolean mAlwaysHideTabBar;
|
||||
protected String[] mPermissionsToHave;
|
||||
|
||||
private CoreListenerStub mListener;
|
||||
private AccountCreatorListenerStub mAccountCreatorListener;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.main);
|
||||
|
||||
mOnBackPressGoHome = true;
|
||||
mAlwaysHideTabBar = false;
|
||||
|
||||
RelativeLayout history = findViewById(R.id.history);
|
||||
history.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(MainActivity.this, HistoryActivity.class);
|
||||
addFlagsToIntent(intent);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
RelativeLayout contacts = findViewById(R.id.contacts);
|
||||
contacts.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(MainActivity.this, ContactsActivity.class);
|
||||
addFlagsToIntent(intent);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
RelativeLayout dialer = findViewById(R.id.dialer);
|
||||
dialer.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(MainActivity.this, DialerActivity.class);
|
||||
addFlagsToIntent(intent);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
RelativeLayout chat = findViewById(R.id.chat);
|
||||
chat.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(MainActivity.this, ChatActivity.class);
|
||||
addFlagsToIntent(intent);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
mMissedCalls = findViewById(R.id.missed_calls);
|
||||
mMissedMessages = findViewById(R.id.missed_chats);
|
||||
|
||||
mHistorySelected = findViewById(R.id.history_select);
|
||||
mContactsSelected = findViewById(R.id.contacts_select);
|
||||
mDialerSelected = findViewById(R.id.dialer_select);
|
||||
mChatSelected = findViewById(R.id.chat_select);
|
||||
|
||||
mTabBar = findViewById(R.id.footer);
|
||||
mTopBar = findViewById(R.id.top_bar);
|
||||
mTopBarTitle = findViewById(R.id.top_bar_title);
|
||||
|
||||
ImageView back = findViewById(R.id.cancel);
|
||||
back.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
goBack();
|
||||
}
|
||||
});
|
||||
|
||||
mStatusBarFragment =
|
||||
(StatusBarFragment) getFragmentManager().findFragmentById(R.id.status_fragment);
|
||||
|
||||
DrawerLayout mSideMenu = findViewById(R.id.side_menu);
|
||||
RelativeLayout mSideMenuContent = findViewById(R.id.side_menu_content);
|
||||
mSideMenuFragment =
|
||||
(SideMenuFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.side_menu_fragment);
|
||||
mSideMenuFragment.setDrawer(mSideMenu, mSideMenuContent);
|
||||
|
||||
if (getResources().getBoolean(R.bool.disable_chat)) {
|
||||
chat.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
mListener =
|
||||
new CoreListenerStub() {
|
||||
@Override
|
||||
public void onCallStateChanged(
|
||||
Core core, Call call, Call.State state, String message) {
|
||||
if (state == Call.State.End || state == Call.State.Released) {
|
||||
displayMissedCalls();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(Core core, ChatRoom room, ChatMessage message) {
|
||||
displayMissedChats();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChatRoomRead(Core core, ChatRoom room) {
|
||||
displayMissedChats();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageReceivedUnableDecrypt(
|
||||
Core core, ChatRoom room, ChatMessage message) {
|
||||
displayMissedChats();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRegistrationStateChanged(
|
||||
Core core,
|
||||
ProxyConfig proxyConfig,
|
||||
RegistrationState state,
|
||||
String message) {
|
||||
mSideMenuFragment.displayAccountsInSideMenu();
|
||||
|
||||
if (state == RegistrationState.Ok) {
|
||||
// For push notifications to work on some devices,
|
||||
// app must be in "protected mode" in battery settings...
|
||||
// https://stackoverflow.com/questions/31638986/protected-apps-setting-on-huawei-phones-and-how-to-handle-it
|
||||
DeviceUtils
|
||||
.displayDialogIfDeviceHasPowerManagerThatCouldPreventPushNotifications(
|
||||
MainActivity.this);
|
||||
|
||||
if (getResources().getBoolean(R.bool.use_phone_number_validation)) {
|
||||
if (proxyConfig
|
||||
.getDomain()
|
||||
.equals(getString(R.string.default_domain))) {
|
||||
isAccountWithAlias();
|
||||
}
|
||||
}
|
||||
|
||||
if (!Compatibility.isDoNotDisturbSettingsAccessGranted(
|
||||
MainActivity.this)) {
|
||||
displayDNDSettingsDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLogCollectionUploadStateChanged(
|
||||
Core core, Core.LogCollectionUploadState state, String info) {
|
||||
Log.d(
|
||||
"[Main Activity] Log upload state: "
|
||||
+ state.toString()
|
||||
+ ", info = "
|
||||
+ info);
|
||||
if (state == Core.LogCollectionUploadState.Delivered) {
|
||||
ClipboardManager clipboard =
|
||||
(ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText("Logs url", info);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Toast.makeText(
|
||||
MainActivity.this,
|
||||
getString(R.string.logs_url_copied_to_clipboard),
|
||||
Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
shareUploadedLogsUrl(info);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mAccountCreatorListener =
|
||||
new AccountCreatorListenerStub() {
|
||||
@Override
|
||||
public void onIsAccountExist(
|
||||
AccountCreator accountCreator,
|
||||
AccountCreator.Status status,
|
||||
String resp) {
|
||||
if (status.equals(AccountCreator.Status.AccountExist)) {
|
||||
accountCreator.isAccountLinked();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLinkAccount(
|
||||
AccountCreator accountCreator,
|
||||
AccountCreator.Status status,
|
||||
String resp) {
|
||||
if (status.equals(AccountCreator.Status.AccountNotLinked)) {
|
||||
askLinkWithPhoneNumber();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onIsAccountLinked(
|
||||
AccountCreator accountCreator,
|
||||
AccountCreator.Status status,
|
||||
String resp) {
|
||||
if (status.equals(AccountCreator.Status.AccountNotLinked)) {
|
||||
askLinkWithPhoneNumber();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
requestRequiredPermissions();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
LinphoneContext.instance()
|
||||
.getNotificationManager()
|
||||
.removeForegroundServiceNotificationIfPossible();
|
||||
|
||||
hideTopBar();
|
||||
if (!mAlwaysHideTabBar
|
||||
&& (getFragmentManager().getBackStackEntryCount() == 0
|
||||
|| !getResources()
|
||||
.getBoolean(R.bool.hide_bottom_bar_on_second_level_views))) {
|
||||
showTabBar();
|
||||
}
|
||||
|
||||
mHistorySelected.setVisibility(View.GONE);
|
||||
mContactsSelected.setVisibility(View.GONE);
|
||||
mDialerSelected.setVisibility(View.GONE);
|
||||
mChatSelected.setVisibility(View.GONE);
|
||||
|
||||
mStatusBarFragment.setMenuListener(this);
|
||||
mSideMenuFragment.setQuitListener(this);
|
||||
mSideMenuFragment.displayAccountsInSideMenu();
|
||||
|
||||
if (mSideMenuFragment.isOpened()) {
|
||||
mSideMenuFragment.closeDrawer();
|
||||
}
|
||||
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
core.addListener(mListener);
|
||||
displayMissedChats();
|
||||
displayMissedCalls();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
mStatusBarFragment.setMenuListener(null);
|
||||
mSideMenuFragment.setQuitListener(null);
|
||||
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
core.removeListener(mListener);
|
||||
}
|
||||
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
mMissedCalls = null;
|
||||
mMissedMessages = null;
|
||||
mContactsSelected = null;
|
||||
mHistorySelected = null;
|
||||
mDialerSelected = null;
|
||||
mChatSelected = null;
|
||||
mTopBar = null;
|
||||
mTopBarTitle = null;
|
||||
mTabBar = null;
|
||||
|
||||
mSideMenuFragment = null;
|
||||
mStatusBarFragment = null;
|
||||
|
||||
mListener = null;
|
||||
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
try {
|
||||
super.onSaveInstanceState(outState);
|
||||
} catch (IllegalStateException ise) {
|
||||
// Do not log this exception
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Bundle savedInstanceState) {
|
||||
try {
|
||||
super.onRestoreInstanceState(savedInstanceState);
|
||||
} catch (IllegalStateException ise) {
|
||||
// Do not log this exception
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMenuCliked() {
|
||||
if (mSideMenuFragment.isOpened()) {
|
||||
mSideMenuFragment.openOrCloseSideMenu(false, true);
|
||||
} else {
|
||||
mSideMenuFragment.openOrCloseSideMenu(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuitClicked() {
|
||||
quit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (mOnBackPressGoHome) {
|
||||
if (getFragmentManager().getBackStackEntryCount() == 0) {
|
||||
goHomeAndClearStack();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
goBack();
|
||||
return true;
|
||||
}
|
||||
return super.onKeyDown(keyCode, event);
|
||||
}
|
||||
|
||||
public boolean popBackStack() {
|
||||
if (getFragmentManager().getBackStackEntryCount() > 0) {
|
||||
getFragmentManager().popBackStackImmediate();
|
||||
if (!mAlwaysHideTabBar
|
||||
&& (getFragmentManager().getBackStackEntryCount() == 0
|
||||
&& getResources()
|
||||
.getBoolean(R.bool.hide_bottom_bar_on_second_level_views))) {
|
||||
showTabBar();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void goBack() {
|
||||
finish();
|
||||
}
|
||||
|
||||
protected boolean isTablet() {
|
||||
return getResources().getBoolean(R.bool.isTablet);
|
||||
}
|
||||
|
||||
private void goHomeAndClearStack() {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
try {
|
||||
startActivity(intent);
|
||||
} catch (IllegalStateException ise) {
|
||||
Log.e("[Main Activity] Can't start home activity: ", ise);
|
||||
}
|
||||
}
|
||||
|
||||
private void quit() {
|
||||
goHomeAndClearStack();
|
||||
if (LinphoneService.isReady()
|
||||
&& LinphonePreferences.instance().getServiceNotificationVisibility()) {
|
||||
LinphoneService.instance().stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
// Tab, Top and Status bars
|
||||
|
||||
public void hideStatusBar() {
|
||||
findViewById(R.id.status_fragment).setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
public void showStatusBar() {
|
||||
findViewById(R.id.status_fragment).setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
public void hideTabBar() {
|
||||
if (!isTablet()) { // do not hide if tablet, otherwise won't be able to navigate...
|
||||
mTabBar.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void showTabBar() {
|
||||
mTabBar.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
protected void hideTopBar() {
|
||||
mTopBar.setVisibility(View.GONE);
|
||||
mTopBarTitle.setText("");
|
||||
}
|
||||
|
||||
private void showTopBar() {
|
||||
mTopBar.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
protected void showTopBarWithTitle(String title) {
|
||||
showTopBar();
|
||||
mTopBarTitle.setText(title);
|
||||
}
|
||||
|
||||
// Permissions
|
||||
|
||||
public boolean checkPermission(String permission) {
|
||||
int granted = getPackageManager().checkPermission(permission, getPackageName());
|
||||
Log.i(
|
||||
"[Permission] "
|
||||
+ permission
|
||||
+ " permission is "
|
||||
+ (granted == PackageManager.PERMISSION_GRANTED ? "granted" : "denied"));
|
||||
return granted == PackageManager.PERMISSION_GRANTED;
|
||||
}
|
||||
|
||||
public boolean checkPermissions(String[] permissions) {
|
||||
boolean allGranted = true;
|
||||
for (String permission : permissions) {
|
||||
allGranted &= checkPermission(permission);
|
||||
}
|
||||
return allGranted;
|
||||
}
|
||||
|
||||
public void requestPermissionIfNotGranted(String permission) {
|
||||
if (!checkPermission(permission)) {
|
||||
Log.i("[Permission] Requesting " + permission + " permission");
|
||||
|
||||
String[] permissions = new String[] {permission};
|
||||
KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
||||
boolean locked = km.inKeyguardRestrictedInputMode();
|
||||
if (!locked) {
|
||||
// This is to workaround an infinite loop of pause/start in Activity issue
|
||||
// if incoming call ends while screen if off and locked
|
||||
ActivityCompat.requestPermissions(this, permissions, FRAGMENT_SPECIFIC_PERMISSION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void requestPermissionsIfNotGranted(String[] perms) {
|
||||
requestPermissionsIfNotGranted(perms, FRAGMENT_SPECIFIC_PERMISSION);
|
||||
}
|
||||
|
||||
private void requestPermissionsIfNotGranted(String[] perms, int resultCode) {
|
||||
ArrayList<String> permissionsToAskFor = new ArrayList<>();
|
||||
if (perms != null) { // This is created (or not) by the child activity
|
||||
for (String permissionToHave : perms) {
|
||||
if (!checkPermission(permissionToHave)) {
|
||||
permissionsToAskFor.add(permissionToHave);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (permissionsToAskFor.size() > 0) {
|
||||
for (String permission : permissionsToAskFor) {
|
||||
Log.i("[Permission] Requesting " + permission + " permission");
|
||||
}
|
||||
String[] permissions = new String[permissionsToAskFor.size()];
|
||||
permissions = permissionsToAskFor.toArray(permissions);
|
||||
|
||||
KeyguardManager km = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
|
||||
boolean locked = km.inKeyguardRestrictedInputMode();
|
||||
if (!locked) {
|
||||
// This is to workaround an infinite loop of pause/start in Activity issue
|
||||
// if incoming call ends while screen if off and locked
|
||||
ActivityCompat.requestPermissions(this, permissions, resultCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requestRequiredPermissions() {
|
||||
requestPermissionsIfNotGranted(mPermissionsToHave, MAIN_PERMISSIONS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, String[] permissions, int[] grantResults) {
|
||||
if (permissions.length <= 0) return;
|
||||
|
||||
for (int i = 0; i < permissions.length; i++) {
|
||||
Log.i(
|
||||
"[Permission] "
|
||||
+ permissions[i]
|
||||
+ " is "
|
||||
+ (grantResults[i] == PackageManager.PERMISSION_GRANTED
|
||||
? "granted"
|
||||
: "denied"));
|
||||
if (permissions[i].equals(Manifest.permission.READ_CONTACTS)
|
||||
|| permissions[i].equals(Manifest.permission.WRITE_CONTACTS)) {
|
||||
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (LinphoneContext.isReady()) {
|
||||
ContactsManager.getInstance().enableContactsAccess();
|
||||
ContactsManager.getInstance().initializeContactManager();
|
||||
ContactsManager.getInstance().fetchContactsAsync();
|
||||
}
|
||||
}
|
||||
} else if (permissions[i].equals(Manifest.permission.READ_EXTERNAL_STORAGE)) {
|
||||
boolean enableRingtone = grantResults[i] == PackageManager.PERMISSION_GRANTED;
|
||||
LinphonePreferences.instance().enableDeviceRingtone(enableRingtone);
|
||||
LinphoneManager.getInstance().enableDeviceRingtone(enableRingtone);
|
||||
} else if (permissions[i].equals(Manifest.permission.CAMERA)
|
||||
&& grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
LinphoneUtils.reloadVideoDevices();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Missed calls & chat indicators
|
||||
|
||||
protected void displayMissedCalls() {
|
||||
int count = 0;
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
count = core.getMissedCallsCount();
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
mMissedCalls.setText(String.valueOf(count));
|
||||
mMissedCalls.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mMissedCalls.clearAnimation();
|
||||
mMissedCalls.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void displayMissedChats() {
|
||||
int count = 0;
|
||||
Core core = LinphoneManager.getCore();
|
||||
if (core != null) {
|
||||
count = core.getUnreadChatMessageCountFromActiveLocals();
|
||||
}
|
||||
|
||||
if (count > 0) {
|
||||
mMissedMessages.setText(String.valueOf(count));
|
||||
mMissedMessages.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mMissedMessages.clearAnimation();
|
||||
mMissedMessages.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
// Navigation between actvities
|
||||
|
||||
public void goBackToCall() {
|
||||
boolean incoming = false;
|
||||
boolean outgoing = false;
|
||||
Call[] calls = LinphoneManager.getCore().getCalls();
|
||||
|
||||
for (Call call : calls) {
|
||||
Call.State state = call.getState();
|
||||
switch (state) {
|
||||
case IncomingEarlyMedia:
|
||||
case IncomingReceived:
|
||||
incoming = true;
|
||||
break;
|
||||
case OutgoingEarlyMedia:
|
||||
case OutgoingInit:
|
||||
case OutgoingProgress:
|
||||
case OutgoingRinging:
|
||||
outgoing = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (incoming) {
|
||||
startActivity(new Intent(this, CallIncomingActivity.class));
|
||||
} else if (outgoing) {
|
||||
startActivity(new Intent(this, CallOutgoingActivity.class));
|
||||
} else {
|
||||
startActivity(new Intent(this, CallActivity.class));
|
||||
}
|
||||
}
|
||||
|
||||
public void newOutgoingCall(String to) {
|
||||
if (LinphoneManager.getCore().getCallsNb() > 0) {
|
||||
Intent intent = new Intent(this, DialerActivity.class);
|
||||
intent.addFlags(
|
||||
Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
intent.putExtra("SipUri", to);
|
||||
this.startActivity(intent);
|
||||
} else {
|
||||
LinphoneManager.getCallManager().newOutgoingCall(to, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void addFlagsToIntent(Intent intent) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||
}
|
||||
|
||||
protected void changeFragment(Fragment fragment, String name, boolean isChild) {
|
||||
FragmentManager fragmentManager = getFragmentManager();
|
||||
FragmentTransaction transaction = fragmentManager.beginTransaction();
|
||||
|
||||
if (transaction.isAddToBackStackAllowed()) {
|
||||
int count = fragmentManager.getBackStackEntryCount();
|
||||
if (count > 0) {
|
||||
FragmentManager.BackStackEntry entry =
|
||||
fragmentManager.getBackStackEntryAt(count - 1);
|
||||
|
||||
if (entry != null && name.equals(entry.getName())) {
|
||||
fragmentManager.popBackStack();
|
||||
if (!isChild) {
|
||||
// We just removed it's duplicate from the back stack
|
||||
// And we want at least one in it
|
||||
transaction.addToBackStack(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isChild) {
|
||||
transaction.addToBackStack(name);
|
||||
}
|
||||
}
|
||||
|
||||
if (getResources().getBoolean(R.bool.hide_bottom_bar_on_second_level_views)) {
|
||||
if (isChild) {
|
||||
if (!isTablet()) {
|
||||
hideTabBar();
|
||||
}
|
||||
} else {
|
||||
showTabBar();
|
||||
}
|
||||
}
|
||||
|
||||
Compatibility.setFragmentTransactionReorderingAllowed(transaction, false);
|
||||
if (isChild && isTablet()) {
|
||||
transaction.replace(R.id.fragmentContainer2, fragment, name);
|
||||
findViewById(R.id.fragmentContainer2).setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
transaction.replace(R.id.fragmentContainer, fragment, name);
|
||||
}
|
||||
transaction.commitAllowingStateLoss();
|
||||
fragmentManager.executePendingTransactions();
|
||||
}
|
||||
|
||||
public void showEmptyChildFragment() {
|
||||
changeFragment(new EmptyFragment(), "Empty", true);
|
||||
}
|
||||
|
||||
public void showAccountSettings(int accountIndex) {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
addFlagsToIntent(intent);
|
||||
intent.putExtra("Account", accountIndex);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void showContactDetails(LinphoneContact contact) {
|
||||
Intent intent = new Intent(this, ContactsActivity.class);
|
||||
addFlagsToIntent(intent);
|
||||
intent.putExtra("Contact", contact);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void showContactsListForCreationOrEdition(Address address) {
|
||||
if (address == null) return;
|
||||
|
||||
Intent intent = new Intent(this, ContactsActivity.class);
|
||||
addFlagsToIntent(intent);
|
||||
intent.putExtra("CreateOrEdit", true);
|
||||
intent.putExtra("SipUri", address.asStringUriOnly());
|
||||
if (address.getDisplayName() != null) {
|
||||
intent.putExtra("DisplayName", address.getDisplayName());
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void showChatRoom(Address localAddress, Address peerAddress) {
|
||||
Intent intent = new Intent(this, ChatActivity.class);
|
||||
addFlagsToIntent(intent);
|
||||
if (localAddress != null) {
|
||||
intent.putExtra("LocalSipUri", localAddress.asStringUriOnly());
|
||||
}
|
||||
if (peerAddress != null) {
|
||||
intent.putExtra("RemoteSipUri", peerAddress.asStringUriOnly());
|
||||
}
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
// Dialogs
|
||||
|
||||
public Dialog displayDialog(String text) {
|
||||
return LinphoneUtils.getDialog(this, text);
|
||||
}
|
||||
|
||||
public void displayChatRoomError() {
|
||||
final Dialog dialog = displayDialog(getString(R.string.chat_room_creation_failed));
|
||||
dialog.findViewById(R.id.dialog_delete_button).setVisibility(View.GONE);
|
||||
Button cancel = dialog.findViewById(R.id.dialog_cancel_button);
|
||||
cancel.setText(getString(R.string.ok));
|
||||
cancel.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void displayDNDSettingsDialog() {
|
||||
if (!LinphonePreferences.instance().isDNDSettingsPopupEnabled()) return;
|
||||
Log.w("[Permission] Asking user to grant us permission to read DND settings");
|
||||
|
||||
final Dialog dialog =
|
||||
displayDialog(getString(R.string.pref_grant_read_dnd_settings_permission_desc));
|
||||
dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE);
|
||||
final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain);
|
||||
dialog.findViewById(R.id.doNotAskAgainLabel)
|
||||
.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
doNotAskAgain.setChecked(!doNotAskAgain.isChecked());
|
||||
}
|
||||
});
|
||||
Button cancel = dialog.findViewById(R.id.dialog_cancel_button);
|
||||
cancel.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (doNotAskAgain.isChecked()) {
|
||||
LinphonePreferences.instance().enableDNDSettingsPopup(false);
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
Button ok = dialog.findViewById(R.id.dialog_ok_button);
|
||||
ok.setVisibility(View.VISIBLE);
|
||||
ok.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
try {
|
||||
startActivity(
|
||||
new Intent(
|
||||
"android.settings.NOTIFICATION_POLICY_ACCESS_SETTINGS"));
|
||||
} catch (ActivityNotFoundException anfe) {
|
||||
Log.e("[Main Activity] Activity not found exception: ", anfe);
|
||||
}
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
Button delete = dialog.findViewById(R.id.dialog_delete_button);
|
||||
delete.setVisibility(View.GONE);
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void isAccountWithAlias() {
|
||||
if (LinphoneManager.getCore().getDefaultProxyConfig() != null) {
|
||||
long now = new Timestamp(new Date().getTime()).getTime();
|
||||
AccountCreator accountCreator = LinphoneManager.getInstance().getAccountCreator();
|
||||
accountCreator.setListener(mAccountCreatorListener);
|
||||
if (LinphonePreferences.instance().getLinkPopupTime() == null
|
||||
|| Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) < now) {
|
||||
accountCreator.reset();
|
||||
accountCreator.setUsername(
|
||||
LinphonePreferences.instance()
|
||||
.getAccountUsername(
|
||||
LinphonePreferences.instance().getDefaultAccountIndex()));
|
||||
accountCreator.isAccountExist();
|
||||
}
|
||||
} else {
|
||||
LinphonePreferences.instance().setLinkPopupTime(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void askLinkWithPhoneNumber() {
|
||||
if (!LinphonePreferences.instance().isLinkPopupEnabled()) return;
|
||||
|
||||
long now = new Timestamp(new Date().getTime()).getTime();
|
||||
if (LinphonePreferences.instance().getLinkPopupTime() != null
|
||||
&& Long.parseLong(LinphonePreferences.instance().getLinkPopupTime()) >= now) return;
|
||||
|
||||
ProxyConfig proxyConfig = LinphoneManager.getCore().getDefaultProxyConfig();
|
||||
if (proxyConfig == null) return;
|
||||
if (!proxyConfig.getDomain().equals(getString(R.string.default_domain))) return;
|
||||
|
||||
final Dialog dialog =
|
||||
LinphoneUtils.getDialog(
|
||||
this,
|
||||
String.format(
|
||||
getString(R.string.link_account_popup),
|
||||
proxyConfig.getIdentityAddress().asStringUriOnly()));
|
||||
Button delete = dialog.findViewById(R.id.dialog_delete_button);
|
||||
delete.setVisibility(View.GONE);
|
||||
Button ok = dialog.findViewById(R.id.dialog_ok_button);
|
||||
ok.setText(getString(R.string.link));
|
||||
ok.setVisibility(View.VISIBLE);
|
||||
Button cancel = dialog.findViewById(R.id.dialog_cancel_button);
|
||||
cancel.setText(getString(R.string.maybe_later));
|
||||
|
||||
dialog.findViewById(R.id.dialog_do_not_ask_again_layout).setVisibility(View.VISIBLE);
|
||||
final CheckBox doNotAskAgain = dialog.findViewById(R.id.doNotAskAgain);
|
||||
dialog.findViewById(R.id.doNotAskAgainLabel)
|
||||
.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
doNotAskAgain.setChecked(!doNotAskAgain.isChecked());
|
||||
}
|
||||
});
|
||||
|
||||
ok.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Intent assistant = new Intent();
|
||||
assistant.setClass(
|
||||
MainActivity.this, PhoneAccountLinkingAssistantActivity.class);
|
||||
startActivity(assistant);
|
||||
updatePopupTimestamp();
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
|
||||
cancel.setOnClickListener(
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (doNotAskAgain.isChecked()) {
|
||||
LinphonePreferences.instance().enableLinkPopup(false);
|
||||
}
|
||||
updatePopupTimestamp();
|
||||
dialog.dismiss();
|
||||
}
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void updatePopupTimestamp() {
|
||||
long future =
|
||||
new Timestamp(
|
||||
getResources()
|
||||
.getInteger(
|
||||
R.integer.phone_number_linking_popup_time_interval))
|
||||
.getTime();
|
||||
long now = new Timestamp(new Date().getTime()).getTime();
|
||||
long newDate = now + future;
|
||||
|
||||
LinphonePreferences.instance().setLinkPopupTime(String.valueOf(newDate));
|
||||
}
|
||||
|
||||
// Logs
|
||||
|
||||
private void shareUploadedLogsUrl(String info) {
|
||||
final String appName = getString(R.string.app_name);
|
||||
|
||||
Intent i = new Intent(Intent.ACTION_SEND);
|
||||
i.putExtra(Intent.EXTRA_EMAIL, new String[] {getString(R.string.about_bugreport_email)});
|
||||
i.putExtra(Intent.EXTRA_SUBJECT, appName + " Logs");
|
||||
i.putExtra(Intent.EXTRA_TEXT, info);
|
||||
i.setType("application/zip");
|
||||
|
||||
try {
|
||||
startActivity(Intent.createChooser(i, "Send mail..."));
|
||||
} catch (android.content.ActivityNotFoundException ex) {
|
||||
Log.e(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// Others
|
||||
|
||||
public SideMenuFragment getSideMenuFragment() {
|
||||
return mSideMenuFragment;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
|
@ -17,8 +17,8 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.call.views;
|
||||
package org.linphone.activities
|
||||
|
||||
public interface CallIncomingButtonListener {
|
||||
void onAction();
|
||||
interface SnackBarActivity {
|
||||
fun showSnackBar(resourceId: Int)
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities;
|
||||
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import org.linphone.R;
|
||||
import org.linphone.settings.LinphonePreferences;
|
||||
|
||||
public abstract class ThemeableActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
if (getResources().getBoolean(R.bool.orientation_portrait_only)) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
int nightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
|
||||
switch (nightMode) {
|
||||
case Configuration.UI_MODE_NIGHT_NO:
|
||||
case Configuration.UI_MODE_NIGHT_UNDEFINED:
|
||||
if (LinphonePreferences.instance().isDarkModeEnabled()) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
}
|
||||
case Configuration.UI_MODE_NIGHT_YES:
|
||||
if (!LinphonePreferences.instance().isDarkModeEnabled()) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.databinding.AssistantActivityBinding
|
||||
|
||||
class AssistantActivity : GenericActivity(), SnackBarActivity {
|
||||
private lateinit var binding: AssistantActivityBinding
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.assistant_activity)
|
||||
|
||||
sharedViewModel = ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
}
|
||||
|
||||
override fun showSnackBar(resourceId: Int) {
|
||||
Snackbar.make(binding.coordinator, resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import android.widget.TextView
|
||||
import kotlin.collections.ArrayList
|
||||
import org.linphone.R
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.core.Factory
|
||||
|
||||
class CountryPickerAdapter : BaseAdapter(), Filterable {
|
||||
private var countries: ArrayList<DialPlan>
|
||||
|
||||
init {
|
||||
val dialPlans = Factory.instance().dialPlans
|
||||
countries = arrayListOf()
|
||||
countries.addAll(dialPlans)
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val view: View = convertView ?: LayoutInflater.from(parent.context).inflate(R.layout.assistant_country_picker_cell, parent, false)
|
||||
val dialPlan: DialPlan = countries[position]
|
||||
|
||||
val name = view.findViewById<TextView>(R.id.country_name)
|
||||
name.text = dialPlan.country
|
||||
|
||||
val dialCode = view.findViewById<TextView>(R.id.country_prefix)
|
||||
dialCode.text = String.format("(%s)", dialPlan.countryCallingCode)
|
||||
|
||||
view.tag = dialPlan
|
||||
return view
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): DialPlan {
|
||||
return countries[position]
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return countries.size
|
||||
}
|
||||
|
||||
override fun getFilter(): Filter {
|
||||
return object : Filter() {
|
||||
override fun performFiltering(constraint: CharSequence): FilterResults {
|
||||
val filteredCountries = arrayListOf<DialPlan>()
|
||||
for (dialPlan in Factory.instance().dialPlans) {
|
||||
if (dialPlan.country.contains(constraint, ignoreCase = true) ||
|
||||
dialPlan.countryCallingCode.contains(constraint)
|
||||
) {
|
||||
filteredCountries.add(dialPlan)
|
||||
}
|
||||
}
|
||||
val filterResults = FilterResults()
|
||||
filterResults.values = filteredCountries
|
||||
return filterResults
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun publishResults(
|
||||
constraint: CharSequence,
|
||||
results: FilterResults
|
||||
) {
|
||||
countries = results.values as ArrayList<DialPlan>
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.Manifest
|
||||
import android.app.AlertDialog
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.viewmodels.AbstractPhoneViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.PermissionHelper
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
abstract class AbstractPhoneFragment : Fragment() {
|
||||
abstract val viewModel: AbstractPhoneViewModel
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == 0) {
|
||||
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.i("[Assistant] READ_PHONE_NUMBERS permission granted")
|
||||
updateFromDeviceInfo()
|
||||
} else {
|
||||
Log.w("[Assistant] READ_PHONE_NUMBERS permission denied")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun checkPermission() {
|
||||
if (!PermissionHelper.get().hasReadPhoneState()) {
|
||||
Log.i("[Assistant] Asking for READ_PHONE_STATE permission")
|
||||
requestPermissions(arrayOf(Manifest.permission.READ_PHONE_STATE), 0)
|
||||
} else {
|
||||
updateFromDeviceInfo()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFromDeviceInfo() {
|
||||
val phoneNumber = PhoneNumberUtils.getDevicePhoneNumber(requireContext())
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanForCurrentCountry(requireContext())
|
||||
viewModel.updateFromPhoneNumberAndOrDialPlan(phoneNumber, dialPlan)
|
||||
}
|
||||
|
||||
protected fun showPhoneNumberInfoDialog() {
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(getString(R.string.assistant_phone_number_info_title))
|
||||
.setMessage(
|
||||
getString(R.string.assistant_phone_number_link_info_content) + "\n" +
|
||||
getString(
|
||||
R.string.assistant_phone_number_link_info_content_already_account
|
||||
)
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.viewmodels.AccountLoginViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.AccountLoginViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.databinding.AssistantAccountLoginFragmentBinding
|
||||
|
||||
class AccountLoginFragment : AbstractPhoneFragment() {
|
||||
private lateinit var binding: AssistantAccountLoginFragmentBinding
|
||||
override lateinit var viewModel: AccountLoginViewModel
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantAccountLoginFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
viewModel = ViewModelProvider(this, AccountLoginViewModelFactory(sharedViewModel.getAccountCreator())).get(AccountLoginViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
if (findNavController().currentDestination?.id == R.id.accountLoginFragment) {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsLogin", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
findNavController().navigate(R.id.action_accountLoginFragment_to_phoneAccountValidationFragment, args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
if (findNavController().currentDestination?.id == R.id.accountLoginFragment) {
|
||||
findNavController().navigate(R.id.action_accountLoginFragment_to_echoCancellerCalibrationFragment)
|
||||
}
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
checkPermission()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import org.linphone.activities.assistant.adapters.CountryPickerAdapter
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.databinding.AssistantCountryPickerFragmentBinding
|
||||
|
||||
class CountryPickerFragment(private val listener: CountryPickedListener) : DialogFragment() {
|
||||
private lateinit var binding: AssistantCountryPickerFragmentBinding
|
||||
private lateinit var adapter: CountryPickerAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantCountryPickerFragmentBinding.inflate(inflater, container, false)
|
||||
|
||||
adapter = CountryPickerAdapter()
|
||||
binding.countryList.adapter = adapter
|
||||
|
||||
binding.countryList.setOnItemClickListener { _, _, position, _ ->
|
||||
if (position > 0 && position < adapter.count) {
|
||||
val dialPlan = adapter.getItem(position)
|
||||
listener.onCountryClicked(dialPlan)
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
|
||||
binding.searchCountry.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
adapter.filter.filter(s)
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { }
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { }
|
||||
})
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
interface CountryPickedListener {
|
||||
fun onCountryClicked(dialPlan: DialPlan)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.activities.assistant.viewmodels.EchoCancellerCalibrationViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantEchoCancellerCalibrationFragmentBinding
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class EchoCancellerCalibrationFragment : Fragment() {
|
||||
private lateinit var binding: AssistantEchoCancellerCalibrationFragmentBinding
|
||||
private lateinit var viewModel: EchoCancellerCalibrationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantEchoCancellerCalibrationFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
viewModel = ViewModelProvider(this).get(EchoCancellerCalibrationViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.echoCalibrationTerminated.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
requireActivity().finish()
|
||||
}
|
||||
})
|
||||
|
||||
if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
|
||||
Log.i("[Echo Canceller Calibration] Asking for RECORD_AUDIO permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.RECORD_AUDIO), 0)
|
||||
} else {
|
||||
viewModel.startEchoCancellerCalibration()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[Echo Canceller Calibration] RECORD_AUDIO permission granted")
|
||||
viewModel.startEchoCancellerCalibration()
|
||||
} else {
|
||||
Log.w("[Echo Canceller Calibration] RECORD_AUDIO permission denied")
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.EmailAccountCreationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.databinding.AssistantEmailAccountCreationFragmentBinding
|
||||
|
||||
class EmailAccountCreationFragment : Fragment() {
|
||||
private lateinit var binding: AssistantEmailAccountCreationFragmentBinding
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: EmailAccountCreationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantEmailAccountCreationFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
viewModel = ViewModelProvider(this, EmailAccountCreationViewModelFactory(sharedViewModel.getAccountCreator())).get(EmailAccountCreationViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.goToEmailValidationEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
if (findNavController().currentDestination?.id == R.id.emailAccountCreationFragment) {
|
||||
findNavController().navigate(R.id.action_emailAccountCreationFragment_to_emailAccountValidationFragment)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.viewmodels.*
|
||||
import org.linphone.databinding.AssistantEmailAccountValidationFragmentBinding
|
||||
|
||||
class EmailAccountValidationFragment : Fragment() {
|
||||
private lateinit var binding: AssistantEmailAccountValidationFragmentBinding
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: EmailAccountValidationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantEmailAccountValidationFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
viewModel = ViewModelProvider(this, EmailAccountValidationViewModelFactory(sharedViewModel.getAccountCreator())).get(EmailAccountValidationViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
if (findNavController().currentDestination?.id == R.id.emailAccountValidationFragment) {
|
||||
val args = Bundle()
|
||||
args.putBoolean("AllowSkip", true)
|
||||
args.putString("Username", viewModel.accountCreator.username)
|
||||
args.putString("Password", viewModel.accountCreator.password)
|
||||
findNavController().navigate(R.id.action_emailAccountValidationFragment_to_phoneAccountLinkingFragment, args)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.viewmodels.GenericLoginViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.GenericLoginViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.databinding.AssistantGenericAccountLoginFragmentBinding
|
||||
|
||||
class GenericAccountLoginFragment : Fragment() {
|
||||
private lateinit var binding: AssistantGenericAccountLoginFragmentBinding
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: GenericLoginViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantGenericAccountLoginFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
viewModel = ViewModelProvider(this, GenericLoginViewModelFactory(sharedViewModel.getAccountCreator(true))).get(GenericLoginViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
if (findNavController().currentDestination?.id == R.id.genericAccountLoginFragment) {
|
||||
findNavController().navigate(R.id.action_genericAccountLoginFragment_to_echoCancellerCalibrationFragment)
|
||||
}
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountCreationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.databinding.AssistantPhoneAccountCreationFragmentBinding
|
||||
|
||||
class PhoneAccountCreationFragment : AbstractPhoneFragment() {
|
||||
private lateinit var binding: AssistantPhoneAccountCreationFragmentBinding
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
override lateinit var viewModel: PhoneAccountCreationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantPhoneAccountCreationFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
viewModel = ViewModelProvider(this, PhoneAccountCreationViewModelFactory(sharedViewModel.getAccountCreator())).get(PhoneAccountCreationViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountCreationFragment) {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsCreation", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
findNavController().navigate(R.id.action_phoneAccountCreationFragment_to_phoneAccountValidationFragment, args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
checkPermission()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.viewmodels.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantPhoneAccountLinkingFragmentBinding
|
||||
|
||||
class PhoneAccountLinkingFragment : AbstractPhoneFragment() {
|
||||
private lateinit var binding: AssistantPhoneAccountLinkingFragmentBinding
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
override lateinit var viewModel: PhoneAccountLinkingViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantPhoneAccountLinkingFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
val accountCreator = sharedViewModel.getAccountCreator()
|
||||
viewModel = ViewModelProvider(this, PhoneAccountLinkingViewModelFactory(accountCreator)).get(PhoneAccountLinkingViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
val username = arguments?.getString("Username")
|
||||
Log.i("[Phone Account Linking] username to link is $username")
|
||||
viewModel.username.value = username
|
||||
|
||||
val password = arguments?.getString("Password")
|
||||
accountCreator.password = password
|
||||
|
||||
val ha1 = arguments?.getString("HA1")
|
||||
accountCreator.ha1 = ha1
|
||||
|
||||
val allowSkip = arguments?.getBoolean("AllowSkip", false)
|
||||
viewModel.allowSkip.value = allowSkip
|
||||
|
||||
binding.setInfoClickListener {
|
||||
showPhoneNumberInfoDialog()
|
||||
}
|
||||
|
||||
binding.setSelectCountryClickListener {
|
||||
CountryPickerFragment(viewModel).show(childFragmentManager, "CountryPicker")
|
||||
}
|
||||
|
||||
viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) {
|
||||
val args = Bundle()
|
||||
args.putBoolean("IsLinking", true)
|
||||
args.putString("PhoneNumber", viewModel.accountCreator.phoneNumber)
|
||||
findNavController().navigate(R.id.action_phoneAccountLinkingFragment_to_phoneAccountValidationFragment, args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountLinkingFragment) {
|
||||
if (LinphoneApplication.coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) {
|
||||
findNavController().navigate(R.id.action_phoneAccountLinkingFragment_to_echoCancellerCalibrationFragment)
|
||||
}
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
checkPermission()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context.CLIPBOARD_SERVICE
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.PhoneAccountValidationViewModelFactory
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.databinding.AssistantPhoneAccountValidationFragmentBinding
|
||||
|
||||
class PhoneAccountValidationFragment : Fragment() {
|
||||
private lateinit var binding: AssistantPhoneAccountValidationFragmentBinding
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: PhoneAccountValidationViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantPhoneAccountValidationFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
viewModel = ViewModelProvider(this, PhoneAccountValidationViewModelFactory(sharedViewModel.getAccountCreator())).get(PhoneAccountValidationViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.phoneNumber.value = arguments?.getString("PhoneNumber")
|
||||
viewModel.isLogin.value = arguments?.getBoolean("IsLogin", false)
|
||||
viewModel.isCreation.value = arguments?.getBoolean("IsCreation", false)
|
||||
viewModel.isLinking.value = arguments?.getBoolean("IsLinking", false)
|
||||
|
||||
viewModel.leaveAssistantEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
when {
|
||||
viewModel.isLogin.value == true || viewModel.isCreation.value == true -> {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) {
|
||||
findNavController().navigate(R.id.action_phoneAccountValidationFragment_to_echoCancellerCalibrationFragment)
|
||||
}
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
viewModel.isLinking.value == true -> {
|
||||
if (findNavController().currentDestination?.id == R.id.phoneAccountValidationFragment) {
|
||||
val args = Bundle()
|
||||
args.putString("Identity", "sip:${viewModel.accountCreator.username}@${viewModel.accountCreator.domain}")
|
||||
findNavController().navigate(R.id.action_phoneAccountValidationFragment_to_accountSettingsFragment, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val clipboard = requireContext().getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
clipboard.addPrimaryClipChangedListener {
|
||||
val data = clipboard.primaryClip
|
||||
if (data != null && data.itemCount > 0) {
|
||||
val clip = data.getItemAt(0).text.toString()
|
||||
if (clip.length == 4) {
|
||||
viewModel.code.value = clip
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.activities.assistant.viewmodels.QrCodeViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AssistantQrCodeFragmentBinding
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class QrCodeFragment : Fragment() {
|
||||
private lateinit var binding: AssistantQrCodeFragmentBinding
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: QrCodeViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantQrCodeFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
viewModel = ViewModelProvider(this).get(QrCodeViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.qrCodeFoundEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { url ->
|
||||
sharedViewModel.remoteProvisioningUrl.value = url
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
})
|
||||
viewModel.setBackCamera()
|
||||
|
||||
if (!PermissionHelper.required(requireContext()).hasRecordAudioPermission()) {
|
||||
Log.i("[QR Code] Asking for CAMERA permission")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.CAMERA), 0)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
coreContext.core.nativePreviewWindowId = binding.qrCodeCaptureTexture
|
||||
coreContext.core.enableQrcodeVideoPreview(true)
|
||||
coreContext.core.enableVideoPreview(true)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.core.nativePreviewWindowId = null
|
||||
coreContext.core.enableQrcodeVideoPreview(false)
|
||||
coreContext.core.enableVideoPreview(false)
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
val granted = grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
if (granted) {
|
||||
Log.i("[QR Code] CAMERA permission granted")
|
||||
} else {
|
||||
Log.w("[QR Code] CAMERA permission denied")
|
||||
findNavController().navigateUp()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Patterns
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.assistant.viewmodels.RemoteProvisioningViewModel
|
||||
import org.linphone.activities.assistant.viewmodels.SharedAssistantViewModel
|
||||
import org.linphone.databinding.AssistantRemoteProvisioningFragmentBinding
|
||||
|
||||
class RemoteProvisioningFragment : Fragment() {
|
||||
private lateinit var binding: AssistantRemoteProvisioningFragmentBinding
|
||||
private lateinit var sharedViewModel: SharedAssistantViewModel
|
||||
private lateinit var viewModel: RemoteProvisioningViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantRemoteProvisioningFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedAssistantViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
viewModel = ViewModelProvider(this).get(RemoteProvisioningViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setApplyClickListener {
|
||||
val url = viewModel.urlToFetch.value.orEmpty()
|
||||
if (Patterns.WEB_URL.matcher(url).matches()) {
|
||||
viewModel.fetchAndApply(url)
|
||||
} else {
|
||||
val activity = requireActivity() as AssistantActivity
|
||||
activity.showSnackBar(R.string.assistant_remote_provisioning_wrong_format)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setQrCodeClickListener {
|
||||
if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) {
|
||||
findNavController().navigate(R.id.action_remoteProvisioningFragment_to_qrCodeFragment)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.fetchSuccessfulEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { success ->
|
||||
if (success) {
|
||||
if (coreContext.core.isEchoCancellerCalibrationRequired) {
|
||||
if (findNavController().currentDestination?.id == R.id.remoteProvisioningFragment) {
|
||||
findNavController().navigate(R.id.action_remoteProvisioningFragment_to_echoCancellerCalibrationFragment)
|
||||
}
|
||||
} else {
|
||||
requireActivity().finish()
|
||||
}
|
||||
} else {
|
||||
val activity = requireActivity() as AssistantActivity
|
||||
activity.showSnackBar(R.string.assistant_remote_provisioning_failure)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.urlToFetch.value = sharedViewModel.remoteProvisioningUrl.value ?: coreContext.core.provisioningUri
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
sharedViewModel.remoteProvisioningUrl.value = null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.databinding.AssistantTopBarFragmentBinding
|
||||
|
||||
class TopBarFragment : Fragment() {
|
||||
private lateinit var binding: AssistantTopBarFragmentBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantTopBarFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
binding.setBackClickListener {
|
||||
if (!findNavController().popBackStack()) {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.databinding.AssistantWelcomeFragmentBinding
|
||||
|
||||
class WelcomeFragment : Fragment() {
|
||||
private lateinit var binding: AssistantWelcomeFragmentBinding
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AssistantWelcomeFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
binding.setCreateAccountClickListener {
|
||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||
if (resources.getBoolean(R.bool.isTablet)) {
|
||||
findNavController().navigate(R.id.action_welcomeFragment_to_emailAccountCreationFragment)
|
||||
} else {
|
||||
findNavController().navigate(R.id.action_welcomeFragment_to_phoneAccountCreationFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.setAccountLoginClickListener {
|
||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||
findNavController().navigate(R.id.action_welcomeFragment_to_accountLoginFragment)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setGenericAccountLoginClickListener {
|
||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||
findNavController().navigate(R.id.action_welcomeFragment_to_genericAccountLoginFragment)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setRemoteProvisioningClickListener {
|
||||
if (findNavController().currentDestination?.id == R.id.welcomeFragment) {
|
||||
findNavController().navigate(R.id.action_welcomeFragment_to_remoteProvisioningFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.activities.assistant.fragments.CountryPickerFragment
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.DialPlan
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.PhoneNumberUtils
|
||||
|
||||
abstract class AbstractPhoneViewModel(val accountCreator: AccountCreator) : ViewModel(),
|
||||
CountryPickerFragment.CountryPickedListener {
|
||||
|
||||
val prefix = MutableLiveData<String>()
|
||||
|
||||
val phoneNumber = MutableLiveData<String>()
|
||||
val phoneNumberError = MutableLiveData<String>()
|
||||
|
||||
val countryName: LiveData<String> = Transformations.switchMap(prefix) {
|
||||
getCountryNameFromPrefix(it)
|
||||
}
|
||||
|
||||
init {
|
||||
prefix.value = "+"
|
||||
}
|
||||
|
||||
override fun onCountryClicked(dialPlan: DialPlan) {
|
||||
prefix.value = "+${dialPlan.countryCallingCode}"
|
||||
}
|
||||
|
||||
fun isPhoneNumberOk(): Boolean {
|
||||
return countryName.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && phoneNumberError.value.orEmpty().isEmpty()
|
||||
}
|
||||
|
||||
fun updateFromPhoneNumberAndOrDialPlan(number: String?, dialPlan: DialPlan?) {
|
||||
if (dialPlan != null) {
|
||||
Log.i("[Assistant] Found prefix from dial plan: ${dialPlan.countryCallingCode}")
|
||||
prefix.value = "+${dialPlan.countryCallingCode}"
|
||||
}
|
||||
if (number != null) {
|
||||
Log.i("[Assistant] Found phone number: $number")
|
||||
phoneNumber.value = number
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCountryNameFromPrefix(prefix: String?): MutableLiveData<String> {
|
||||
val country = MutableLiveData<String>()
|
||||
country.value = ""
|
||||
|
||||
if (prefix != null && prefix.isNotEmpty()) {
|
||||
val countryCode = if (prefix.first() == '+') prefix.substring(1) else prefix
|
||||
val dialPlan = PhoneNumberUtils.getDialPlanFromCountryCallingPrefix(countryCode)
|
||||
Log.i("[Assistant] Found dial plan $dialPlan from country code: $countryCode")
|
||||
country.value = dialPlan?.country
|
||||
}
|
||||
return country
|
||||
}
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.ProxyConfig
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class AccountLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return AccountLoginViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class AccountLoginViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val loginWithUsernamePassword = MutableLiveData<Boolean>()
|
||||
|
||||
val username = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
|
||||
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onRecoverAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Login] Recover account status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.RequestOk) {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
} else {
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
loginWithUsernamePassword.value = false
|
||||
|
||||
loginEnabled.value = false
|
||||
loginEnabled.addSource(prefix) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(phoneNumber) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(username) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(password) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(loginWithUsernamePassword) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(phoneNumberError) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun login() {
|
||||
if (loginWithUsernamePassword.value == true) {
|
||||
accountCreator.username = username.value
|
||||
accountCreator.password = password.value
|
||||
Log.i("[Assistant] [Account Login] Username is ${accountCreator.username}")
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
} else {
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
accountCreator.username = accountCreator.phoneNumber
|
||||
Log.i("[Assistant] [Account Login] Phone number is ${accountCreator.phoneNumber}")
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.recoverAccount()
|
||||
Log.i("[Assistant] [Account Login] Recover account returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLoginButtonEnabled(): Boolean {
|
||||
return if (loginWithUsernamePassword.value == true) {
|
||||
username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
|
||||
} else {
|
||||
isPhoneNumberOk()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProxyConfig(): Boolean {
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Account Login] Account creator couldn't create proxy config")
|
||||
// TODO: show error
|
||||
return false
|
||||
}
|
||||
|
||||
proxyConfig.isPushNotificationAllowed = true
|
||||
|
||||
Log.i("[Assistant] [Account Login] Proxy config created")
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.EcCalibratorStatus
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EchoCancellerCalibrationViewModel : ViewModel() {
|
||||
val echoCalibrationTerminated = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onEcCalibrationResult(core: Core, status: EcCalibratorStatus, delayMs: Int) {
|
||||
if (status == EcCalibratorStatus.InProgress) return
|
||||
echoCancellerCalibrationFinished(status, delayMs)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
}
|
||||
|
||||
fun startEchoCancellerCalibration() {
|
||||
coreContext.core.startEchoCancellerCalibration()
|
||||
}
|
||||
|
||||
fun echoCancellerCalibrationFinished(status: EcCalibratorStatus, delay: Int) {
|
||||
coreContext.core.removeListener(listener)
|
||||
when (status) {
|
||||
EcCalibratorStatus.DoneNoEcho -> {
|
||||
Log.i("[Echo Canceller Calibration] Done, no echo")
|
||||
}
|
||||
EcCalibratorStatus.Done -> {
|
||||
Log.i("[Echo Canceller Calibration] Done, delay is ${delay}ms")
|
||||
}
|
||||
EcCalibratorStatus.Failed -> {
|
||||
Log.w("[Echo Canceller Calibration] Failed")
|
||||
}
|
||||
}
|
||||
echoCalibrationTerminated.value = Event(true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EmailAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return EmailAccountCreationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EmailAccountCreationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val username = MutableLiveData<String>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val email = MutableLiveData<String>()
|
||||
val emailError = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
val passwordError = MutableLiveData<String>()
|
||||
|
||||
val passwordConfirmation = MutableLiveData<String>()
|
||||
val passwordConfirmationError = MutableLiveData<String>()
|
||||
|
||||
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val goToEmailValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountExist(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Account Creation] onIsAccountExist status is $status")
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||
waitForServerAnswer.value = false
|
||||
usernameError.value = AppUtils.getString(R.string.assistant_error_username_already_exists)
|
||||
}
|
||||
AccountCreator.Status.AccountNotExist -> {
|
||||
val createAccountStatus = creator.createAccount()
|
||||
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Account Creation] onCreateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountCreated -> {
|
||||
goToEmailValidationEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
createEnabled.value = false
|
||||
createEnabled.addSource(username) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(usernameError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(email) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(emailError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(password) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordConfirmation) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(passwordConfirmationError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun create() {
|
||||
accountCreator.username = username.value
|
||||
accountCreator.password = password.value
|
||||
accountCreator.email = email.value
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountExist
|
||||
Log.i("[Assistant] [Account Creation] Account exists returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCreateButtonEnabled(): Boolean {
|
||||
return username.value.orEmpty().isNotEmpty() &&
|
||||
email.value.orEmpty().isNotEmpty() &&
|
||||
password.value.orEmpty().isNotEmpty() &&
|
||||
passwordConfirmation.value.orEmpty().isNotEmpty() &&
|
||||
password.value == passwordConfirmation.value &&
|
||||
usernameError.value.orEmpty().isEmpty() &&
|
||||
emailError.value.orEmpty().isEmpty() &&
|
||||
passwordError.value.orEmpty().isEmpty() &&
|
||||
passwordConfirmationError.value.orEmpty().isEmpty()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.ProxyConfig
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class EmailAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return EmailAccountValidationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EmailAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val email = MutableLiveData<String>()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountActivated(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Account Validation] onIsAccountActivated status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountActivated -> {
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AccountNotActivated -> {
|
||||
// TODO: show error
|
||||
}
|
||||
else -> {
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
email.value = accountCreator.email
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountActivated
|
||||
Log.i("[Assistant] [Account Validation] Account exists returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProxyConfig(): Boolean {
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Account Validation] Account creator couldn't create proxy config")
|
||||
// TODO: show error
|
||||
return false
|
||||
}
|
||||
|
||||
proxyConfig.isPushNotificationAllowed = true
|
||||
|
||||
Log.i("[Assistant] [Account Validation] Proxy config created")
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.ProxyConfig
|
||||
import org.linphone.core.TransportType
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class GenericLoginViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return GenericLoginViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class GenericLoginViewModel(private val accountCreator: AccountCreator) : ViewModel() {
|
||||
val username = MutableLiveData<String>()
|
||||
|
||||
val password = MutableLiveData<String>()
|
||||
|
||||
val domain = MutableLiveData<String>()
|
||||
|
||||
val displayName = MutableLiveData<String>()
|
||||
|
||||
val transport = MutableLiveData<TransportType>()
|
||||
|
||||
val loginEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
init {
|
||||
transport.value = TransportType.Tls
|
||||
|
||||
loginEnabled.value = false
|
||||
loginEnabled.addSource(username) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(password) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
loginEnabled.addSource(domain) {
|
||||
loginEnabled.value = isLoginButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
fun setTransport(transportType: TransportType) {
|
||||
transport.value = transportType
|
||||
}
|
||||
|
||||
fun createProxyConfig() {
|
||||
accountCreator.username = username.value
|
||||
accountCreator.password = password.value
|
||||
accountCreator.domain = domain.value
|
||||
accountCreator.displayName = displayName.value
|
||||
accountCreator.transport = transport.value
|
||||
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Generic Login] Account creator couldn't create proxy config")
|
||||
// TODO: show error
|
||||
return
|
||||
}
|
||||
|
||||
Log.i("[Assistant] [Generic Login] Proxy config created")
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private fun isLoginButtonEnabled(): Boolean {
|
||||
return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountCreationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountCreationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountCreationViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val username = MutableLiveData<String>()
|
||||
val useUsername = MutableLiveData<Boolean>()
|
||||
val usernameError = MutableLiveData<String>()
|
||||
|
||||
val createEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val goToSmsValidationEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAccountExist(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Creation] onIsAccountExist status is $status")
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
|
||||
waitForServerAnswer.value = false
|
||||
if (useUsername.value == true) {
|
||||
usernameError.value = AppUtils.getString(R.string.assistant_error_username_already_exists)
|
||||
} else {
|
||||
phoneNumberError.value = AppUtils.getString(R.string.assistant_error_phone_number_already_exists)
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AccountNotExist -> {
|
||||
val createAccountStatus = creator.createAccount()
|
||||
Log.i("[Phone Account Creation] createAccount returned $createAccountStatus")
|
||||
if (createAccountStatus != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Creation] onCreateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountCreated -> {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
}
|
||||
AccountCreator.Status.AccountExistWithAlias -> {
|
||||
phoneNumberError.value = AppUtils.getString(R.string.assistant_error_phone_number_already_exists)
|
||||
}
|
||||
else -> {
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
useUsername.value = false
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
createEnabled.value = false
|
||||
createEnabled.addSource(prefix) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(phoneNumber) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(useUsername) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(username) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(usernameError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
createEnabled.addSource(phoneNumberError) {
|
||||
createEnabled.value = isCreateButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun create() {
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
if (useUsername.value == true) {
|
||||
accountCreator.username = username.value
|
||||
} else {
|
||||
accountCreator.username = accountCreator.phoneNumber
|
||||
}
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status = accountCreator.isAccountExist
|
||||
Log.i("[Phone Account Creation] isAccountExist returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
|
||||
private fun isCreateButtonEnabled(): Boolean {
|
||||
val usernameRegexp = corePreferences.config.getString("assistant", "username_regex", "^[a-z0-9+_.\\-]*\$")
|
||||
return isPhoneNumberOk() &&
|
||||
(useUsername.value == false ||
|
||||
username.value.orEmpty().matches(Regex(usernameRegexp)) &&
|
||||
username.value.orEmpty().isNotEmpty() &&
|
||||
usernameError.value.orEmpty().isEmpty())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.*
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountLinkingViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountLinkingViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountLinkingViewModel(accountCreator: AccountCreator) : AbstractPhoneViewModel(accountCreator) {
|
||||
val username = MutableLiveData<String>()
|
||||
|
||||
val allowSkip = MutableLiveData<Boolean>()
|
||||
|
||||
val linkEnabled: MediatorLiveData<Boolean> = MediatorLiveData()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val goToSmsValidationEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onIsAliasUsed(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Linking] onIsAliasUsed status is $status")
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AliasNotExist -> {
|
||||
if (creator.linkAccount() != AccountCreator.Status.RequestOk) {
|
||||
Log.e("[Phone Account Linking] linkAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
AccountCreator.Status.AliasExist, AccountCreator.Status.AliasIsAccount -> {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
else -> {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLinkAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Phone Account Linking] onLinkAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.RequestOk -> {
|
||||
goToSmsValidationEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
|
||||
linkEnabled.value = false
|
||||
linkEnabled.addSource(prefix) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
linkEnabled.addSource(phoneNumber) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
linkEnabled.addSource(phoneNumberError) {
|
||||
linkEnabled.value = isLinkButtonEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun link() {
|
||||
accountCreator.setPhoneNumber(phoneNumber.value, prefix.value)
|
||||
accountCreator.username = username.value
|
||||
Log.i("[Assistant] [Phone Account Linking] Phone number is ${accountCreator.phoneNumber}")
|
||||
|
||||
waitForServerAnswer.value = true
|
||||
val status: AccountCreator.Status = accountCreator.isAliasUsed
|
||||
Log.i("[Phone Account Linking] isAliasUsed returned $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
|
||||
fun skip() {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
|
||||
private fun isLinkButtonEnabled(): Boolean {
|
||||
return isPhoneNumberOk()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.core.AccountCreator
|
||||
import org.linphone.core.AccountCreatorListenerStub
|
||||
import org.linphone.core.ProxyConfig
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class PhoneAccountValidationViewModelFactory(private val accountCreator: AccountCreator) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return PhoneAccountValidationViewModel(accountCreator) as T
|
||||
}
|
||||
}
|
||||
|
||||
class PhoneAccountValidationViewModel(val accountCreator: AccountCreator) : ViewModel() {
|
||||
val phoneNumber = MutableLiveData<String>()
|
||||
|
||||
val code = MutableLiveData<String>()
|
||||
|
||||
val isLogin = MutableLiveData<Boolean>()
|
||||
|
||||
val isCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val isLinking = MutableLiveData<Boolean>()
|
||||
|
||||
val waitForServerAnswer = MutableLiveData<Boolean>()
|
||||
|
||||
val leaveAssistantEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
val listener = object : AccountCreatorListenerStub() {
|
||||
override fun onLoginLinphoneAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onLoginLinphoneAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.RequestOk) {
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
// TODO: show error
|
||||
}
|
||||
} else {
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivateAlias(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onActivateAlias status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
when (status) {
|
||||
AccountCreator.Status.AccountActivated -> {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
}
|
||||
else -> {
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onActivateAccount(
|
||||
creator: AccountCreator,
|
||||
status: AccountCreator.Status,
|
||||
response: String?
|
||||
) {
|
||||
Log.i("[Assistant] [Phone Account Validation] onActivateAccount status is $status")
|
||||
waitForServerAnswer.value = false
|
||||
|
||||
if (status == AccountCreator.Status.AccountActivated) {
|
||||
if (createProxyConfig()) {
|
||||
leaveAssistantEvent.value = Event(true)
|
||||
} else {
|
||||
// TODO: show error
|
||||
}
|
||||
} else {
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
accountCreator.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountCreator.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun finish() {
|
||||
accountCreator.activationCode = code.value.orEmpty()
|
||||
Log.i("[Assistant] [Phone Account Validation] Phone number is ${accountCreator.phoneNumber} and activation code is ${accountCreator.activationCode}")
|
||||
waitForServerAnswer.value = true
|
||||
|
||||
val status = when {
|
||||
isLogin.value == true -> accountCreator.loginLinphoneAccount()
|
||||
isCreation.value == true -> accountCreator.activateAccount()
|
||||
isLinking.value == true -> accountCreator.activateAlias()
|
||||
else -> AccountCreator.Status.UnexpectedError
|
||||
}
|
||||
Log.i("[Assistant] [Phone Account Validation] Code validation result is $status")
|
||||
if (status != AccountCreator.Status.RequestOk) {
|
||||
waitForServerAnswer.value = false
|
||||
// TODO: show error
|
||||
}
|
||||
}
|
||||
|
||||
private fun createProxyConfig(): Boolean {
|
||||
val proxyConfig: ProxyConfig? = accountCreator.createProxyConfig()
|
||||
|
||||
if (proxyConfig == null) {
|
||||
Log.e("[Assistant] [Phone Account Validation] Account creator couldn't create proxy config")
|
||||
// TODO: show error
|
||||
return false
|
||||
}
|
||||
|
||||
proxyConfig.isPushNotificationAllowed = true
|
||||
Log.i("[Assistant] [Phone Account Validation] Proxy config created")
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class QrCodeViewModel : ViewModel() {
|
||||
val qrCodeFoundEvent = MutableLiveData<Event<String>>()
|
||||
|
||||
val showSwitchCamera = MutableLiveData<Boolean>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onQrcodeFound(core: Core, result: String) {
|
||||
Log.i("[QR Code] Found [$result]")
|
||||
qrCodeFoundEvent.postValue(Event(result))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
showSwitchCamera.value = coreContext.core.videoDevicesList.size > 1
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun setBackCamera() {
|
||||
for (camera in coreContext.core.videoDevicesList) {
|
||||
if (camera.contains("Back")) {
|
||||
Log.i("[QR Code] Found back facing camera: $camera")
|
||||
coreContext.core.videoDevice = camera
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val first = coreContext.core.videoDevicesList[0]
|
||||
Log.i("[QR Code] Using first camera found: $first")
|
||||
coreContext.core.videoDevice = first
|
||||
}
|
||||
|
||||
fun switchCamera() {
|
||||
coreContext.switchCamera()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.ConfiguringState
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class RemoteProvisioningViewModel : ViewModel() {
|
||||
val urlToFetch = MutableLiveData<String>()
|
||||
val fetchInProgress = MutableLiveData<Boolean>()
|
||||
val fetchSuccessfulEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onConfiguringStatus(core: Core, status: ConfiguringState, message: String?) {
|
||||
fetchInProgress.value = false
|
||||
when (status) {
|
||||
ConfiguringState.Successful -> {
|
||||
fetchSuccessfulEvent.value = Event(true)
|
||||
}
|
||||
ConfiguringState.Failed -> {
|
||||
fetchSuccessfulEvent.value = Event(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
fetchInProgress.value = false
|
||||
coreContext.core.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun fetchAndApply(url: String) {
|
||||
coreContext.core.provisioningUri = url
|
||||
Log.w("[Remote Provisioning] Url set to [$url], restarting Core")
|
||||
fetchInProgress.value = true
|
||||
coreContext.core.stop()
|
||||
coreContext.core.start()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.assistant.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.util.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class SharedAssistantViewModel : ViewModel() {
|
||||
val remoteProvisioningUrl = MutableLiveData<String>()
|
||||
|
||||
private var accountCreator: AccountCreator
|
||||
private var useGenericSipAccount: Boolean = false
|
||||
|
||||
init {
|
||||
Log.i("[Assistant] Loading linphone default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||
accountCreator = coreContext.core.createAccountCreator(corePreferences.xmlRpcServerUrl)
|
||||
accountCreator.language = Locale.getDefault().language
|
||||
}
|
||||
|
||||
fun getAccountCreator(genericAccountCreator: Boolean = false): AccountCreator {
|
||||
if (genericAccountCreator != useGenericSipAccount) {
|
||||
accountCreator.reset()
|
||||
accountCreator.language = Locale.getDefault().language
|
||||
|
||||
if (genericAccountCreator) {
|
||||
Log.i("[Assistant] Loading default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.defaultValuesPath)
|
||||
} else {
|
||||
Log.i("[Assistant] Loading linphone default values")
|
||||
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
|
||||
}
|
||||
useGenericSipAccount = genericAccountCreator
|
||||
}
|
||||
return accountCreator
|
||||
}
|
||||
}
|
205
app/src/main/java/org/linphone/activities/call/CallActivity.kt
Normal file
205
app/src/main/java/org/linphone/activities/call/CallActivity.kt
Normal file
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.hardware.Sensor
|
||||
import android.hardware.SensorEvent
|
||||
import android.hardware.SensorEventListener
|
||||
import android.hardware.SensorManager
|
||||
import android.os.Bundle
|
||||
import android.os.PowerManager
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.call.viewmodels.ControlsFadingViewModel
|
||||
import org.linphone.activities.call.viewmodels.SharedCallViewModel
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallActivityBinding
|
||||
|
||||
class CallActivity : GenericActivity() {
|
||||
private lateinit var binding: CallActivityBinding
|
||||
private lateinit var viewModel: ControlsFadingViewModel
|
||||
private lateinit var sharedViewModel: SharedCallViewModel
|
||||
|
||||
private var previewX: Float = 0f
|
||||
private var previewY: Float = 0f
|
||||
private lateinit var videoZoomHelper: VideoZoomHelper
|
||||
|
||||
private lateinit var sensorManager: SensorManager
|
||||
private lateinit var proximitySensor: Sensor
|
||||
private lateinit var proximityWakeLock: PowerManager.WakeLock
|
||||
private val proximityListener: SensorEventListener = object : SensorEventListener {
|
||||
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { }
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
if (event.timestamp == 0L) return
|
||||
if (isProximitySensorNearby(event)) {
|
||||
if (!proximityWakeLock.isHeld) {
|
||||
Log.i("[Call Activity] Acquiring proximity wake lock")
|
||||
proximityWakeLock.acquire()
|
||||
}
|
||||
} else {
|
||||
if (proximityWakeLock.isHeld) {
|
||||
Log.i("[Call Activity] Releasing proximity wake lock")
|
||||
proximityWakeLock.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private var proximitySensorEnabled = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
|
||||
proximitySensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
|
||||
proximityWakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
|
||||
.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "$packageName;proximity_sensor")
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.call_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
viewModel = ViewModelProvider(this).get(ControlsFadingViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
sharedViewModel = ViewModelProvider(this).get(SharedCallViewModel::class.java)
|
||||
|
||||
sharedViewModel.toggleDrawerEvent.observe(this, Observer {
|
||||
it.consume {
|
||||
if (binding.statsMenu.isDrawerOpen(Gravity.LEFT)) {
|
||||
binding.statsMenu.closeDrawer(binding.sideMenuContent, true)
|
||||
} else {
|
||||
binding.statsMenu.openDrawer(binding.sideMenuContent, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
coreContext.core.nativeVideoWindowId = binding.remoteVideoSurface
|
||||
coreContext.core.nativePreviewWindowId = binding.localPreviewVideoSurface
|
||||
|
||||
binding.setPreviewTouchListener { v, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
previewX = v.x - event.rawX
|
||||
previewY = v.y - event.rawY
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
v.animate().x(event.rawX + previewX).y(event.rawY + previewY).setDuration(0).start()
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
videoZoomHelper = VideoZoomHelper(this, binding.remoteVideoSurface)
|
||||
|
||||
viewModel.videoEnabledEvent.observe(this, Observer {
|
||||
it.consume { videoEnabled ->
|
||||
enableProximitySensor(!videoEnabled)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (coreContext.core.callsNb == 0) {
|
||||
Log.w("[Call Activity] Resuming but no call found...")
|
||||
finish()
|
||||
} else {
|
||||
coreContext.removeCallOverlay()
|
||||
|
||||
val currentCall = coreContext.core.currentCall ?: coreContext.core.calls[0]
|
||||
if (currentCall != null) {
|
||||
val videoEnabled = currentCall.currentParams.videoEnabled()
|
||||
enableProximitySensor(!videoEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
enableProximitySensor(false)
|
||||
|
||||
val core = coreContext.core
|
||||
if (core.callsNb > 0) {
|
||||
coreContext.createCallOverlay()
|
||||
}
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
|
||||
if (coreContext.core.currentCall?.currentParams?.videoEnabled() == true) {
|
||||
Compatibility.enterPipMode(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration
|
||||
) {
|
||||
if (isInPictureInPictureMode) {
|
||||
viewModel.areControlsHidden.value = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableProximitySensor(enable: Boolean) {
|
||||
if (enable) {
|
||||
if (!proximitySensorEnabled) {
|
||||
Log.i("[Call Activity] Enabling proximity sensor listener")
|
||||
sensorManager.registerListener(proximityListener, proximitySensor, SensorManager.SENSOR_DELAY_NORMAL)
|
||||
proximitySensorEnabled = true
|
||||
}
|
||||
} else {
|
||||
if (proximitySensorEnabled) {
|
||||
Log.i("[Call Activity] Disabling proximity sensor listener")
|
||||
sensorManager.unregisterListener(proximityListener)
|
||||
if (proximityWakeLock.isHeld) {
|
||||
proximityWakeLock.release()
|
||||
}
|
||||
proximitySensorEnabled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isProximitySensorNearby(event: SensorEvent): Boolean {
|
||||
var threshold = 4.001f // <= 4 cm is near
|
||||
|
||||
val distanceInCm = event.values[0]
|
||||
val maxDistance = event.sensor.maximumRange
|
||||
Log.d("[Call Activity] Proximity sensor report [$distanceInCm] , for max range [$maxDistance]")
|
||||
|
||||
if (maxDistance <= threshold) {
|
||||
// Case binary 0/1 and short sensors
|
||||
threshold = maxDistance
|
||||
}
|
||||
return distanceInCm < threshold
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.app.KeyguardManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.call.viewmodels.IncomingCallViewModel
|
||||
import org.linphone.activities.call.viewmodels.IncomingCallViewModelFactory
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallIncomingActivityBinding
|
||||
import org.linphone.mediastream.Version
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class IncomingCallActivity : GenericActivity() {
|
||||
private lateinit var binding: CallIncomingActivityBinding
|
||||
private lateinit var viewModel: IncomingCallViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.call_incoming_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
var incomingCall: Call? = null
|
||||
for (call in coreContext.core.calls) {
|
||||
if (call.state == Call.State.IncomingReceived ||
|
||||
call.state == Call.State.IncomingEarlyMedia) {
|
||||
incomingCall = call
|
||||
}
|
||||
}
|
||||
|
||||
if (incomingCall == null) {
|
||||
Log.e("[Incoming Call] Couldn't find call in state Incoming")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
IncomingCallViewModelFactory(incomingCall)
|
||||
)[IncomingCallViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.callEndedEvent.observe(this, Observer {
|
||||
it.consume {
|
||||
finish()
|
||||
}
|
||||
})
|
||||
|
||||
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
viewModel.screenLocked.value = keyguardManager.isKeyguardLocked
|
||||
|
||||
binding.buttons.setViewModel(viewModel)
|
||||
|
||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Version.API23_MARSHMALLOW_60)
|
||||
private fun checkPermissions() {
|
||||
val permissionsRequiredList = arrayListOf<String>()
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
Log.i("[Incoming Call] Asking for RECORD_AUDIO permission")
|
||||
permissionsRequiredList.add(android.Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) {
|
||||
Log.i("[Incoming Call] Asking for CAMERA permission")
|
||||
permissionsRequiredList.add(android.Manifest.permission.CAMERA)
|
||||
}
|
||||
if (permissionsRequiredList.isNotEmpty()) {
|
||||
val permissionsRequired = arrayOfNulls<String>(permissionsRequiredList.size)
|
||||
permissionsRequiredList.toArray(permissionsRequired)
|
||||
requestPermissions(permissionsRequired, 0)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.os.Bundle
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.call.viewmodels.CallViewModel
|
||||
import org.linphone.activities.call.viewmodels.CallViewModelFactory
|
||||
import org.linphone.activities.call.viewmodels.ControlsViewModel
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallOutgoingActivityBinding
|
||||
import org.linphone.mediastream.Version
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class OutgoingCallActivity : GenericActivity() {
|
||||
private lateinit var binding: CallOutgoingActivityBinding
|
||||
private lateinit var viewModel: CallViewModel
|
||||
private lateinit var controlsViewModel: ControlsViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.call_outgoing_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
var outgoingCall: Call? = null
|
||||
for (call in coreContext.core.calls) {
|
||||
if (call.state == Call.State.OutgoingInit ||
|
||||
call.state == Call.State.OutgoingProgress ||
|
||||
call.state == Call.State.OutgoingRinging) {
|
||||
outgoingCall = call
|
||||
}
|
||||
}
|
||||
|
||||
if (outgoingCall == null) {
|
||||
Log.e("[Outgoing Call] Couldn't find call in state Outgoing")
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
CallViewModelFactory(outgoingCall)
|
||||
)[CallViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
controlsViewModel = ViewModelProvider(this).get(ControlsViewModel::class.java)
|
||||
binding.controlsViewModel = controlsViewModel
|
||||
|
||||
binding.setTerminateCallClickListener {
|
||||
viewModel.terminateCall()
|
||||
}
|
||||
|
||||
binding.setToggleMicrophoneClickListener {
|
||||
if (PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
controlsViewModel.toggleMuteMicrophone()
|
||||
} else {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
binding.setToggleSpeakerClickListener {
|
||||
controlsViewModel.toggleSpeaker()
|
||||
}
|
||||
|
||||
viewModel.callEndedEvent.observe(this, Observer {
|
||||
it.consume {
|
||||
finish()
|
||||
}
|
||||
})
|
||||
|
||||
if (Version.sdkAboveOrEqual(Version.API23_MARSHMALLOW_60)) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Version.API23_MARSHMALLOW_60)
|
||||
private fun checkPermissions() {
|
||||
val permissionsRequiredList = arrayListOf<String>()
|
||||
if (!PermissionHelper.get().hasRecordAudioPermission()) {
|
||||
Log.i("[Outgoing Call] Asking for RECORD_AUDIO permission")
|
||||
permissionsRequiredList.add(android.Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
if (viewModel.call.currentParams.videoEnabled() && !PermissionHelper.get().hasCameraPermission()) {
|
||||
Log.i("[Outgoing Call] Asking for CAMERA permission")
|
||||
permissionsRequiredList.add(android.Manifest.permission.CAMERA)
|
||||
}
|
||||
if (permissionsRequiredList.isNotEmpty()) {
|
||||
val permissionsRequired = arrayOfNulls<String>(permissionsRequiredList.size)
|
||||
permissionsRequiredList.toArray(permissionsRequired)
|
||||
requestPermissions(permissionsRequired, 0)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call
|
||||
|
||||
import android.content.Context
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.View
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Call
|
||||
|
||||
class VideoZoomHelper(context: Context, private var videoDisplayView: View) : GestureDetector.SimpleOnGestureListener() {
|
||||
private var scaleDetector: ScaleGestureDetector
|
||||
|
||||
private var zoomFactor = 1f
|
||||
private var zoomCenterX = 0f
|
||||
private var zoomCenterY = 0f
|
||||
|
||||
init {
|
||||
val gestureDetector = GestureDetector(context, this)
|
||||
|
||||
scaleDetector = ScaleGestureDetector(context, object :
|
||||
ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
zoomFactor *= detector.scaleFactor
|
||||
// Don't let the object get too small or too large.
|
||||
// Zoom to make the video fill the screen vertically
|
||||
val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
|
||||
// Zoom to make the video fill the screen horizontally
|
||||
val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
|
||||
zoomFactor = max(0.1f, min(zoomFactor, max(portraitZoomFactor, landscapeZoomFactor)))
|
||||
|
||||
val currentCall: Call? = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
videoDisplayView.setOnTouchListener { v, event ->
|
||||
val currentZoomFactor = zoomFactor
|
||||
scaleDetector.onTouchEvent(event)
|
||||
|
||||
if (currentZoomFactor != zoomFactor) {
|
||||
// We did scale, prevent touch event from going further
|
||||
return@setOnTouchListener true
|
||||
}
|
||||
|
||||
// If true, gesture detected, prevent touch event from going further
|
||||
// Otherwise it seems we didn't use event,
|
||||
// allow it to be dispatched somewhere else
|
||||
gestureDetector.onTouchEvent(event)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent,
|
||||
e2: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
val currentCall: Call? = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
if (zoomFactor > 1) {
|
||||
// Video is zoomed, slide is used to change center of zoom
|
||||
if (distanceX > 0 && zoomCenterX < 1) {
|
||||
zoomCenterX += 0.01f
|
||||
} else if (distanceX < 0 && zoomCenterX > 0) {
|
||||
zoomCenterX -= 0.01f
|
||||
}
|
||||
|
||||
if (distanceY < 0 && zoomCenterY < 1) {
|
||||
zoomCenterY += 0.01f
|
||||
} else if (distanceY > 0 && zoomCenterY > 0) {
|
||||
zoomCenterY -= 0.01f
|
||||
}
|
||||
|
||||
if (zoomCenterX > 1) zoomCenterX = 1f
|
||||
if (zoomCenterX < 0) zoomCenterX = 0f
|
||||
if (zoomCenterY > 1) zoomCenterY = 1f
|
||||
if (zoomCenterY < 0) zoomCenterY = 0f
|
||||
|
||||
currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDoubleTap(e: MotionEvent?): Boolean {
|
||||
val currentCall: Call? = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
if (zoomFactor == 1f) {
|
||||
// Zoom to make the video fill the screen vertically
|
||||
val portraitZoomFactor = videoDisplayView.height.toFloat() / (3 * videoDisplayView.width / 4)
|
||||
// Zoom to make the video fill the screen horizontally
|
||||
val landscapeZoomFactor = videoDisplayView.width.toFloat() / (3 * videoDisplayView.height / 4)
|
||||
zoomFactor = max(portraitZoomFactor, landscapeZoomFactor)
|
||||
} else {
|
||||
resetZoom()
|
||||
}
|
||||
|
||||
currentCall.zoom(zoomFactor, zoomCenterX, zoomCenterY)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun resetZoom() {
|
||||
zoomFactor = 1f
|
||||
zoomCenterY = 0.5f
|
||||
zoomCenterX = zoomCenterY
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.fragments
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.activities.call.viewmodels.CallsViewModel
|
||||
import org.linphone.activities.call.viewmodels.ControlsViewModel
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.databinding.CallControlsFragmentBinding
|
||||
|
||||
class ControlsFragment : Fragment() {
|
||||
private lateinit var binding: CallControlsFragmentBinding
|
||||
private lateinit var callsViewModel: CallsViewModel
|
||||
private lateinit var controlsViewModel: ControlsViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = CallControlsFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
callsViewModel = ViewModelProvider(this).get(CallsViewModel::class.java)
|
||||
binding.viewModel = callsViewModel
|
||||
|
||||
controlsViewModel = ViewModelProvider(this).get(ControlsViewModel::class.java)
|
||||
binding.controlsViewModel = controlsViewModel
|
||||
|
||||
callsViewModel.currentCallViewModel.observe(viewLifecycleOwner, Observer {
|
||||
if (it != null) {
|
||||
binding.activeCallTimer.base =
|
||||
SystemClock.elapsedRealtime() - (1000 * it.call.duration) // Linphone timestamps are in seconds
|
||||
binding.activeCallTimer.start()
|
||||
}
|
||||
})
|
||||
|
||||
callsViewModel.noMoreCallEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
activity?.finish()
|
||||
}
|
||||
})
|
||||
|
||||
controlsViewModel.chatClickedEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
val intent = Intent()
|
||||
intent.setClass(requireContext(), MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
intent.putExtra("Chat", true)
|
||||
startActivity(intent)
|
||||
}
|
||||
})
|
||||
|
||||
controlsViewModel.addCallClickedEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
val intent = Intent()
|
||||
intent.setClass(requireContext(), MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
intent.putExtra("Dialer", true)
|
||||
intent.putExtra("Transfer", false)
|
||||
startActivity(intent)
|
||||
}
|
||||
})
|
||||
|
||||
controlsViewModel.transferCallClickedEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
val intent = Intent()
|
||||
intent.setClass(requireContext(), MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
intent.putExtra("Dialer", true)
|
||||
intent.putExtra("Transfer", true)
|
||||
startActivity(intent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.activities.call.viewmodels.StatisticsListViewModel
|
||||
import org.linphone.databinding.CallStatisticsFragmentBinding
|
||||
|
||||
class StatisticsFragment : Fragment() {
|
||||
private lateinit var binding: CallStatisticsFragmentBinding
|
||||
private lateinit var viewModel: StatisticsListViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = CallStatisticsFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
viewModel = ViewModelProvider(this).get(StatisticsListViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
}
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.util.*
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.call.viewmodels.SharedCallViewModel
|
||||
import org.linphone.activities.call.viewmodels.StatusViewModel
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallStatusFragmentBinding
|
||||
import org.linphone.utils.DialogUtils
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class StatusFragment : Fragment() {
|
||||
private lateinit var binding: CallStatusFragmentBinding
|
||||
private lateinit var viewModel: StatusViewModel
|
||||
private lateinit var sharedViewModel: SharedCallViewModel
|
||||
private var zrtpDialog: Dialog? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = CallStatusFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
viewModel = ViewModelProvider(this).get(StatusViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedCallViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
binding.setStatsClickListener {
|
||||
sharedViewModel.toggleDrawerEvent.value = Event(true)
|
||||
}
|
||||
|
||||
binding.setRefreshClickListener {
|
||||
viewModel.refreshRegister()
|
||||
}
|
||||
|
||||
viewModel.showZrtpDialogEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { call ->
|
||||
if (call.state == Call.State.Connected || call.state == Call.State.StreamsRunning) {
|
||||
showZrtpDialog(call)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (zrtpDialog != null) {
|
||||
zrtpDialog?.dismiss()
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun showZrtpDialog(call: Call) {
|
||||
if (zrtpDialog != null && zrtpDialog?.isShowing == true) {
|
||||
Log.e("[Status Fragment] ZRTP dialog already visible")
|
||||
return
|
||||
}
|
||||
|
||||
val token = call.authenticationToken
|
||||
if (token == null || token.length < 4) {
|
||||
Log.e("[Status Fragment] ZRTP token is invalid: $token")
|
||||
return
|
||||
}
|
||||
|
||||
val toRead: String
|
||||
val toListen: String
|
||||
when (call.dir) {
|
||||
Call.Dir.Incoming -> {
|
||||
toRead = token.substring(0, 2)
|
||||
toListen = token.substring(2)
|
||||
}
|
||||
else -> {
|
||||
toRead = token.substring(2)
|
||||
toListen = token.substring(0, 2)
|
||||
}
|
||||
}
|
||||
|
||||
val viewModel = DialogViewModel(getString(R.string.zrtp_dialog_message), getString(R.string.zrtp_dialog_title))
|
||||
viewModel.showZrtp = true
|
||||
viewModel.zrtpReadSas = toRead.toUpperCase(Locale.getDefault())
|
||||
viewModel.zrtpListenSas = toListen.toUpperCase(Locale.getDefault())
|
||||
viewModel.showIcon = true
|
||||
viewModel.iconResource = R.drawable.security_2_indicator
|
||||
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||
|
||||
viewModel.showDeleteButton({
|
||||
call.authenticationTokenVerified = false
|
||||
this@StatusFragment.viewModel.updateEncryptionInfo(call)
|
||||
dialog.dismiss()
|
||||
zrtpDialog = null
|
||||
}, getString(R.string.zrtp_dialog_deny_button_label))
|
||||
|
||||
viewModel.showOkButton({
|
||||
call.authenticationTokenVerified = true
|
||||
this@StatusFragment.viewModel.updateEncryptionInfo(call)
|
||||
dialog.dismiss()
|
||||
zrtpDialog = null
|
||||
}, getString(R.string.zrtp_dialog_ok_button_label))
|
||||
|
||||
zrtpDialog = dialog
|
||||
dialog.show()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.contact.GenericContactViewModel
|
||||
import org.linphone.core.*
|
||||
|
||||
class CallStatisticsViewModel(val call: Call) : GenericContactViewModel(call.remoteAddress) {
|
||||
val audioStats = MutableLiveData<ArrayList<StatItemViewModel>>()
|
||||
|
||||
val videoStats = MutableLiveData<ArrayList<StatItemViewModel>>()
|
||||
|
||||
val isVideoEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isExpanded = MutableLiveData<Boolean>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
|
||||
if (call == this@CallStatisticsViewModel.call) {
|
||||
isVideoEnabled.value = call.currentParams.videoEnabled()
|
||||
updateCallStats(stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
audioStats.value = arrayListOf()
|
||||
videoStats.value = arrayListOf()
|
||||
|
||||
initCallStats()
|
||||
|
||||
val videoEnabled = call.currentParams.videoEnabled()
|
||||
isVideoEnabled.value = videoEnabled
|
||||
|
||||
isExpanded.value = coreContext.core.currentCall == call
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun toggleExpanded() {
|
||||
isExpanded.value = isExpanded.value != true
|
||||
}
|
||||
|
||||
private fun initCallStats() {
|
||||
val audioList = arrayListOf<StatItemViewModel>()
|
||||
audioList.add(StatItemViewModel(StatType.CAPTURE))
|
||||
audioList.add(StatItemViewModel(StatType.PLAYBACK))
|
||||
audioList.add(StatItemViewModel(StatType.PAYLOAD))
|
||||
audioList.add(StatItemViewModel(StatType.ENCODER))
|
||||
audioList.add(StatItemViewModel(StatType.DECODER))
|
||||
audioList.add(StatItemViewModel(StatType.DOWNLOAD_BW))
|
||||
audioList.add(StatItemViewModel(StatType.UPLOAD_BW))
|
||||
audioList.add(StatItemViewModel(StatType.ICE))
|
||||
audioList.add(StatItemViewModel(StatType.IP_FAM))
|
||||
audioList.add(StatItemViewModel(StatType.SENDER_LOSS))
|
||||
audioList.add(StatItemViewModel(StatType.RECEIVER_LOSS))
|
||||
audioList.add(StatItemViewModel(StatType.JITTER))
|
||||
audioStats.value = audioList
|
||||
|
||||
val videoList = arrayListOf<StatItemViewModel>()
|
||||
videoList.add(StatItemViewModel(StatType.CAPTURE))
|
||||
videoList.add(StatItemViewModel(StatType.PLAYBACK))
|
||||
videoList.add(StatItemViewModel(StatType.PAYLOAD))
|
||||
videoList.add(StatItemViewModel(StatType.ENCODER))
|
||||
videoList.add(StatItemViewModel(StatType.DECODER))
|
||||
videoList.add(StatItemViewModel(StatType.DOWNLOAD_BW))
|
||||
videoList.add(StatItemViewModel(StatType.UPLOAD_BW))
|
||||
videoList.add(StatItemViewModel(StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW))
|
||||
videoList.add(StatItemViewModel(StatType.ICE))
|
||||
videoList.add(StatItemViewModel(StatType.IP_FAM))
|
||||
videoList.add(StatItemViewModel(StatType.SENDER_LOSS))
|
||||
videoList.add(StatItemViewModel(StatType.RECEIVER_LOSS))
|
||||
videoList.add(StatItemViewModel(StatType.SENT_RESOLUTION))
|
||||
videoList.add(StatItemViewModel(StatType.RECEIVED_RESOLUTION))
|
||||
videoList.add(StatItemViewModel(StatType.SENT_FPS))
|
||||
videoList.add(StatItemViewModel(StatType.RECEIVED_FPS))
|
||||
videoStats.value = videoList
|
||||
}
|
||||
|
||||
private fun updateCallStats(stats: CallStats) {
|
||||
if (stats.type == StreamType.Audio) {
|
||||
for (stat in audioStats.value.orEmpty()) {
|
||||
stat.update(call, stats)
|
||||
}
|
||||
} else if (stats.type == StreamType.Video) {
|
||||
for (stat in videoStats.value.orEmpty()) {
|
||||
stat.update(call, stats)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.contact.GenericContactViewModel
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.CallListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class CallViewModelFactory(private val call: Call) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return CallViewModel(call) as T
|
||||
}
|
||||
}
|
||||
|
||||
open class CallViewModel(val call: Call) : GenericContactViewModel(call.remoteAddress) {
|
||||
val address: String by lazy {
|
||||
call.remoteAddress.clean() // To remove gruu if any
|
||||
call.remoteAddress.asStringUriOnly()
|
||||
}
|
||||
|
||||
val isPaused = MutableLiveData<Boolean>()
|
||||
|
||||
val callEndedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val listener = object : CallListenerStub() {
|
||||
override fun onStateChanged(call: Call, state: Call.State, message: String) {
|
||||
if (call != this@CallViewModel.call) return
|
||||
|
||||
isPaused.value = state == Call.State.Paused
|
||||
|
||||
if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
|
||||
callEndedEvent.value = Event(true)
|
||||
|
||||
if (state == Call.State.Error) {
|
||||
Log.e("[Call View Model] Error state reason is ${call.reason}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
call.addListener(listener)
|
||||
|
||||
isPaused.value = call.state == Call.State.Paused
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
call.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun terminateCall() {
|
||||
coreContext.terminateCall(call)
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
call.pause()
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
call.resume()
|
||||
}
|
||||
|
||||
fun removeFromConference() {
|
||||
if (call.conference != null) {
|
||||
call.conference.removeParticipant(call.remoteAddress)
|
||||
if (call.core.conferenceSize <= 1) call.core.leaveConference()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class CallsViewModel : ViewModel() {
|
||||
val currentCallViewModel = MutableLiveData<CallViewModel>()
|
||||
|
||||
val callPausedByRemote = MutableLiveData<Boolean>()
|
||||
|
||||
val pausedCalls = MutableLiveData<ArrayList<CallViewModel>>()
|
||||
|
||||
val conferenceCalls = MutableLiveData<ArrayList<CallViewModel>>()
|
||||
|
||||
val isConferencePaused = MutableLiveData<Boolean>()
|
||||
|
||||
val noMoreCallEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String
|
||||
) {
|
||||
callPausedByRemote.value = state == Call.State.PausedByRemote
|
||||
isConferencePaused.value = !coreContext.core.isInConference
|
||||
|
||||
if (core.currentCall == null) {
|
||||
currentCallViewModel.value = null
|
||||
} else if (currentCallViewModel.value == null) {
|
||||
currentCallViewModel.value = CallViewModel(core.currentCall)
|
||||
}
|
||||
|
||||
if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error) {
|
||||
if (core.callsNb == 0) {
|
||||
noMoreCallEvent.value = Event(true)
|
||||
conferenceCalls.value = arrayListOf()
|
||||
} else {
|
||||
removeCallFromPausedListIfPresent(call)
|
||||
removeCallFromConferenceIfPresent(call)
|
||||
}
|
||||
} else {
|
||||
if (state == Call.State.Pausing) {
|
||||
addCallToPausedList(call)
|
||||
} else if (state == Call.State.Resuming) {
|
||||
removeCallFromPausedListIfPresent(call)
|
||||
} else {
|
||||
if (call.conference != null) {
|
||||
addCallToConferenceListIfNotAlreadyInIt(call)
|
||||
} else {
|
||||
removeCallFromConferenceIfPresent(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
val currentCall = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
currentCallViewModel.value = CallViewModel(currentCall)
|
||||
}
|
||||
callPausedByRemote.value = currentCall?.state == Call.State.PausedByRemote
|
||||
isConferencePaused.value = !coreContext.core.isInConference
|
||||
|
||||
val conferenceList = arrayListOf<CallViewModel>()
|
||||
for (call in coreContext.core.calls) {
|
||||
if (call.state == Call.State.Paused || call.state == Call.State.Pausing) {
|
||||
addCallToPausedList(call)
|
||||
} else {
|
||||
if (call.conference != null && call.core.isInConference) {
|
||||
conferenceList.add(CallViewModel(call))
|
||||
}
|
||||
}
|
||||
}
|
||||
conferenceCalls.value = conferenceList
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun pauseConference() {
|
||||
if (coreContext.core.isInConference) {
|
||||
coreContext.core.leaveConference()
|
||||
isConferencePaused.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun resumeConference() {
|
||||
if (!coreContext.core.isInConference) {
|
||||
coreContext.core.enterConference()
|
||||
isConferencePaused.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCallToPausedList(call: Call) {
|
||||
val list = arrayListOf<CallViewModel>()
|
||||
list.addAll(pausedCalls.value.orEmpty())
|
||||
|
||||
val viewModel = CallViewModel(call)
|
||||
list.add(viewModel)
|
||||
pausedCalls.value = list
|
||||
}
|
||||
|
||||
private fun removeCallFromPausedListIfPresent(call: Call) {
|
||||
val list = arrayListOf<CallViewModel>()
|
||||
list.addAll(pausedCalls.value.orEmpty())
|
||||
|
||||
for (pausedCallViewModel in list) {
|
||||
if (pausedCallViewModel.call == call) {
|
||||
list.remove(pausedCallViewModel)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
pausedCalls.value = list
|
||||
}
|
||||
|
||||
private fun addCallToConferenceListIfNotAlreadyInIt(call: Call) {
|
||||
val list = arrayListOf<CallViewModel>()
|
||||
list.addAll(conferenceCalls.value.orEmpty())
|
||||
|
||||
for (viewModel in list) {
|
||||
if (viewModel.call == call) return
|
||||
}
|
||||
|
||||
val viewModel = CallViewModel(call)
|
||||
list.add(viewModel)
|
||||
conferenceCalls.value = list
|
||||
}
|
||||
|
||||
private fun removeCallFromConferenceIfPresent(call: Call) {
|
||||
val list = arrayListOf<CallViewModel>()
|
||||
list.addAll(conferenceCalls.value.orEmpty())
|
||||
|
||||
for (viewModel in list) {
|
||||
if (viewModel.call == call) {
|
||||
list.remove(viewModel)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
conferenceCalls.value = list
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.util.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class ControlsFadingViewModel : ViewModel() {
|
||||
val areControlsHidden = MutableLiveData<Boolean>()
|
||||
|
||||
val videoEnabledEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
private var timer: Timer? = null
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String?
|
||||
) {
|
||||
if (state == Call.State.StreamsRunning || state == Call.State.Updating || state == Call.State.UpdatedByRemote) {
|
||||
Log.i("[Controls Fading] Call is in state $state, video is enabled? ${call.currentParams.videoEnabled()}")
|
||||
if (call.currentParams.videoEnabled()) {
|
||||
videoEnabledEvent.value = Event(true)
|
||||
startTimer()
|
||||
} else {
|
||||
videoEnabledEvent.value = Event(false)
|
||||
stopTimer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
areControlsHidden.value = false
|
||||
|
||||
val currentCall = coreContext.core.currentCall
|
||||
if (currentCall != null && currentCall.currentParams.videoEnabled()) {
|
||||
videoEnabledEvent.value = Event(true)
|
||||
startTimer()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
stopTimer()
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun showMomentarily() {
|
||||
stopTimer()
|
||||
startTimer()
|
||||
}
|
||||
|
||||
private fun stopTimer() {
|
||||
timer?.cancel()
|
||||
|
||||
areControlsHidden.value = false
|
||||
}
|
||||
|
||||
private fun startTimer() {
|
||||
timer?.cancel()
|
||||
|
||||
timer = Timer("Hide UI controls scheduler")
|
||||
timer?.schedule(object : TimerTask() {
|
||||
override fun run() {
|
||||
areControlsHidden.postValue(coreContext.core.currentCall?.currentParams?.videoEnabled() ?: false)
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,317 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import kotlin.math.max
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.activities.main.dialer.NumpadDigitListener
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ControlsViewModel : ViewModel() {
|
||||
val isMicrophoneMuted = MutableLiveData<Boolean>()
|
||||
|
||||
val isMuteMicrophoneEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isSpeakerSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val isBluetoothHeadsetSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val isVideoAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val isVideoEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isVideoUpdateInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val isPauseEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isRecording = MutableLiveData<Boolean>()
|
||||
|
||||
val isConferencingAvailable = MutableLiveData<Boolean>()
|
||||
|
||||
val unreadMessagesCount = MutableLiveData<Int>()
|
||||
|
||||
val numpadVisibility = MutableLiveData<Boolean>()
|
||||
|
||||
val optionsVisibility = MutableLiveData<Boolean>()
|
||||
|
||||
val audioRoutesVisibility = MutableLiveData<Boolean>()
|
||||
|
||||
val audioRoutesEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val chatClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val addCallClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val transferCallClickedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val onKeyClick: NumpadDigitListener = object : NumpadDigitListener {
|
||||
override fun handleClick(key: Char) {
|
||||
coreContext.core.playDtmf(key, 1)
|
||||
}
|
||||
|
||||
override fun handleLongClick(key: Char): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||
updateUnreadChatCount()
|
||||
}
|
||||
|
||||
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
|
||||
updateUnreadChatCount()
|
||||
}
|
||||
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String?
|
||||
) {
|
||||
if (state == Call.State.StreamsRunning) isVideoUpdateInProgress.value = false
|
||||
|
||||
updateUI()
|
||||
}
|
||||
|
||||
override fun onAudioDeviceChanged(core: Core, audioDevice: AudioDevice) {
|
||||
updateAudioRelated()
|
||||
}
|
||||
|
||||
override fun onAudioDevicesListUpdated(core: Core) {
|
||||
updateAudioRelated()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
val currentCall = coreContext.core.currentCall
|
||||
|
||||
updateMuteMicState()
|
||||
updateAudioRelated()
|
||||
updateUnreadChatCount()
|
||||
|
||||
numpadVisibility.value = false
|
||||
optionsVisibility.value = false
|
||||
audioRoutesVisibility.value = false
|
||||
|
||||
isRecording.value = currentCall?.isRecording
|
||||
isVideoUpdateInProgress.value = false
|
||||
|
||||
updateUI()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun updateUnreadChatCount() {
|
||||
unreadMessagesCount.value = coreContext.core.unreadChatMessageCountFromActiveLocals
|
||||
}
|
||||
|
||||
fun toggleMuteMicrophone() {
|
||||
val micEnabled = coreContext.core.micEnabled()
|
||||
coreContext.core.enableMic(!micEnabled)
|
||||
updateMuteMicState()
|
||||
}
|
||||
|
||||
fun toggleSpeaker() {
|
||||
val audioDevice = coreContext.core.outputAudioDevice
|
||||
if (audioDevice?.type == AudioDevice.Type.Speaker) {
|
||||
forceEarpieceAudioRoute()
|
||||
} else {
|
||||
forceSpeakerAudioRoute()
|
||||
}
|
||||
}
|
||||
|
||||
fun switchCamera() {
|
||||
coreContext.switchCamera()
|
||||
}
|
||||
|
||||
fun terminateCall() {
|
||||
val core = coreContext.core
|
||||
when {
|
||||
core.currentCall != null -> core.currentCall.terminate()
|
||||
core.isInConference -> core.terminateConference()
|
||||
else -> core.terminateAllCalls()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleVideo() {
|
||||
val core = coreContext.core
|
||||
val currentCall = core.currentCall
|
||||
|
||||
if (currentCall != null) {
|
||||
val state = currentCall.state
|
||||
if (state == Call.State.End || state == Call.State.Released || state == Call.State.Error)
|
||||
return
|
||||
|
||||
isVideoUpdateInProgress.value = true
|
||||
val params = core.createCallParams(currentCall)
|
||||
params.enableVideo(!currentCall.currentParams.videoEnabled())
|
||||
currentCall.update(params)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleOptionsMenu() {
|
||||
optionsVisibility.value = optionsVisibility.value != true
|
||||
}
|
||||
|
||||
fun toggleNumpadVisibility() {
|
||||
numpadVisibility.value = numpadVisibility.value != true
|
||||
}
|
||||
|
||||
fun toggleRoutesMenu() {
|
||||
audioRoutesVisibility.value = audioRoutesVisibility.value != true
|
||||
}
|
||||
|
||||
fun toggleRecording(closeMenu: Boolean) {
|
||||
val currentCall = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
if (currentCall.isRecording) {
|
||||
currentCall.stopRecording()
|
||||
} else {
|
||||
currentCall.startRecording()
|
||||
}
|
||||
}
|
||||
isRecording.value = currentCall?.isRecording
|
||||
if (closeMenu) toggleOptionsMenu()
|
||||
}
|
||||
|
||||
fun onChatClicked() {
|
||||
chatClickedEvent.value = Event(true)
|
||||
}
|
||||
|
||||
fun onAddCallClicked() {
|
||||
addCallClickedEvent.value = Event(true)
|
||||
toggleOptionsMenu()
|
||||
}
|
||||
|
||||
fun onTransferCallClicked() {
|
||||
transferCallClickedEvent.value = Event(true)
|
||||
toggleOptionsMenu()
|
||||
}
|
||||
|
||||
fun startConference() {
|
||||
coreContext.core.addAllToConference()
|
||||
toggleOptionsMenu()
|
||||
}
|
||||
|
||||
fun forceEarpieceAudioRoute() {
|
||||
for (audioDevice in coreContext.core.audioDevices) {
|
||||
if (audioDevice.type == AudioDevice.Type.Earpiece) {
|
||||
Log.i("[Call] Found earpiece audio device [${audioDevice.deviceName}], routing audio to it")
|
||||
coreContext.core.outputAudioDevice = audioDevice
|
||||
return
|
||||
}
|
||||
}
|
||||
Log.e("[Call] Couldn't find earpiece audio device")
|
||||
}
|
||||
|
||||
fun forceSpeakerAudioRoute() {
|
||||
for (audioDevice in coreContext.core.audioDevices) {
|
||||
if (audioDevice.type == AudioDevice.Type.Speaker) {
|
||||
Log.i("[Call] Found speaker audio device [${audioDevice.deviceName}], routing audio to it")
|
||||
coreContext.core.outputAudioDevice = audioDevice
|
||||
return
|
||||
}
|
||||
}
|
||||
Log.e("[Call] Couldn't find speaker audio device")
|
||||
}
|
||||
|
||||
fun forceBluetoothAudioRoute() {
|
||||
for (audioDevice in coreContext.core.audioDevices) {
|
||||
if (audioDevice.type == AudioDevice.Type.Bluetooth) {
|
||||
Log.i("[Call] Found bluetooth audio device [${audioDevice.deviceName}], routing audio to it")
|
||||
coreContext.core.outputAudioDevice = audioDevice
|
||||
return
|
||||
}
|
||||
}
|
||||
Log.e("[Call] Couldn't find bluetooth audio device")
|
||||
}
|
||||
|
||||
private fun updateAudioRelated() {
|
||||
updateSpeakerState()
|
||||
updateBluetoothHeadsetState()
|
||||
updateAudioRoutesState()
|
||||
}
|
||||
|
||||
private fun updateUI() {
|
||||
val currentCall = coreContext.core.currentCall
|
||||
updateVideoAvailable()
|
||||
updateVideoEnabled()
|
||||
isPauseEnabled.value = currentCall != null && !currentCall.mediaInProgress()
|
||||
isMuteMicrophoneEnabled.value = currentCall != null || coreContext.core.isInConference
|
||||
updateConferenceState()
|
||||
}
|
||||
|
||||
private fun updateMuteMicState() {
|
||||
isMicrophoneMuted.value = !PermissionHelper.get().hasRecordAudioPermission() || !coreContext.core.micEnabled()
|
||||
}
|
||||
|
||||
private fun updateSpeakerState() {
|
||||
val audioDevice = coreContext.core.outputAudioDevice
|
||||
isSpeakerSelected.value = audioDevice?.type == AudioDevice.Type.Speaker
|
||||
}
|
||||
|
||||
private fun updateAudioRoutesState() {
|
||||
var bluetoothDeviceAvailable = false
|
||||
for (audioDevice in coreContext.core.audioDevices) {
|
||||
if (audioDevice.type == AudioDevice.Type.Bluetooth) {
|
||||
bluetoothDeviceAvailable = true
|
||||
break
|
||||
}
|
||||
}
|
||||
audioRoutesEnabled.value = bluetoothDeviceAvailable
|
||||
}
|
||||
|
||||
private fun updateBluetoothHeadsetState() {
|
||||
val audioDevice = coreContext.core.outputAudioDevice
|
||||
isBluetoothHeadsetSelected.value = audioDevice?.type == AudioDevice.Type.Bluetooth
|
||||
}
|
||||
|
||||
private fun updateVideoAvailable() {
|
||||
val core = coreContext.core
|
||||
isVideoAvailable.value = (core.videoCaptureEnabled() || core.videoPreviewEnabled()) &&
|
||||
core.currentCall != null && !core.currentCall.mediaInProgress()
|
||||
}
|
||||
|
||||
private fun updateVideoEnabled() {
|
||||
val core = coreContext.core
|
||||
isVideoEnabled.value = core.currentCall?.currentParams?.videoEnabled()
|
||||
}
|
||||
|
||||
private fun updateConferenceState() {
|
||||
val core = coreContext.core
|
||||
isConferencingAvailable.value = core.callsNb > max(1, core.conferenceSize) && !core.soundResourcesLocked()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.*
|
||||
|
||||
class IncomingCallViewModelFactory(private val call: Call) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return IncomingCallViewModel(call) as T
|
||||
}
|
||||
}
|
||||
|
||||
class IncomingCallViewModel(call: Call) : CallViewModel(call) {
|
||||
val screenLocked = MutableLiveData<Boolean>()
|
||||
|
||||
val earlyMediaVideoEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val inviteWithVideo = MutableLiveData<Boolean>()
|
||||
|
||||
init {
|
||||
screenLocked.value = false
|
||||
inviteWithVideo.value = call.currentParams.videoEnabled()
|
||||
earlyMediaVideoEnabled.value = call.state == Call.State.IncomingEarlyMedia && call.currentParams?.videoEnabled() ?: false
|
||||
}
|
||||
|
||||
fun answer(doAction: Boolean) {
|
||||
if (doAction) coreContext.answerCall(call)
|
||||
}
|
||||
|
||||
fun decline(doAction: Boolean) {
|
||||
if (doAction) coreContext.declineCall(call)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
|
@ -17,8 +17,12 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.compatibility;
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
public interface CompatibilityScaleGestureListener {
|
||||
boolean onScale(CompatibilityScaleGestureDetector detector);
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class SharedCallViewModel : ViewModel() {
|
||||
val toggleDrawerEvent = MutableLiveData<Event<Boolean>>()
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.text.DecimalFormat
|
||||
import org.linphone.R
|
||||
import org.linphone.core.AddressFamily
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.CallStats
|
||||
import org.linphone.core.StreamType
|
||||
|
||||
enum class StatType(val nameResource: Int) {
|
||||
CAPTURE(R.string.call_stats_capture_filter),
|
||||
PLAYBACK(R.string.call_stats_player_filter),
|
||||
PAYLOAD(R.string.call_stats_codec),
|
||||
ENCODER(R.string.call_stats_encoder_name),
|
||||
DECODER(R.string.call_stats_decoder_name),
|
||||
DOWNLOAD_BW(R.string.call_stats_download),
|
||||
UPLOAD_BW(R.string.call_stats_upload),
|
||||
ICE(R.string.call_stats_ice),
|
||||
IP_FAM(R.string.call_stats_ip),
|
||||
SENDER_LOSS(R.string.call_stats_sender_loss_rate),
|
||||
RECEIVER_LOSS(R.string.call_stats_receiver_loss_rate),
|
||||
JITTER(R.string.call_stats_jitter_buffer),
|
||||
SENT_RESOLUTION(R.string.call_stats_video_resolution_sent),
|
||||
RECEIVED_RESOLUTION(R.string.call_stats_video_resolution_received),
|
||||
SENT_FPS(R.string.call_stats_video_fps_sent),
|
||||
RECEIVED_FPS(R.string.call_stats_video_fps_received),
|
||||
ESTIMATED_AVAILABLE_DOWNLOAD_BW(R.string.call_stats_estimated_download)
|
||||
}
|
||||
|
||||
class StatItemViewModel(val type: StatType) : ViewModel() {
|
||||
val value = MutableLiveData<String>()
|
||||
|
||||
fun update(call: Call, stats: CallStats) {
|
||||
val payloadType = if (stats.type == StreamType.Audio) call.currentParams.usedAudioPayloadType else call.currentParams.usedVideoPayloadType
|
||||
value.value = when (type) {
|
||||
StatType.CAPTURE -> if (stats.type == StreamType.Audio) call.core.captureDevice else call.core.videoDevice
|
||||
StatType.PLAYBACK -> if (stats.type == StreamType.Audio) call.core.playbackDevice else call.core.videoDisplayFilter
|
||||
StatType.PAYLOAD -> "${payloadType.mimeType}/${payloadType.clockRate / 1000} kHz"
|
||||
StatType.ENCODER -> call.core.mediastreamerFactory.getDecoderText(payloadType.mimeType)
|
||||
StatType.DECODER -> call.core.mediastreamerFactory.getEncoderText(payloadType.mimeType)
|
||||
StatType.DOWNLOAD_BW -> "${stats.downloadBandwidth} kbits/s"
|
||||
StatType.UPLOAD_BW -> "${stats.uploadBandwidth} kbits/s"
|
||||
StatType.ICE -> stats.iceState.toString()
|
||||
StatType.IP_FAM -> if (stats.ipFamilyOfRemote == AddressFamily.Inet6) "IPv6" else "IPv4"
|
||||
StatType.SENDER_LOSS -> DecimalFormat("##.##%").format(stats.senderLossRate)
|
||||
StatType.RECEIVER_LOSS -> DecimalFormat("##.##%").format(stats.receiverLossRate)
|
||||
StatType.JITTER -> DecimalFormat("##.## ms").format(stats.jitterBufferSizeMs)
|
||||
StatType.SENT_RESOLUTION -> call.currentParams.sentVideoDefinition?.name
|
||||
StatType.RECEIVED_RESOLUTION -> call.currentParams.receivedVideoDefinition?.name
|
||||
StatType.SENT_FPS -> "${call.currentParams.sentFramerate}"
|
||||
StatType.RECEIVED_FPS -> "${call.currentParams.receivedFramerate}"
|
||||
StatType.ESTIMATED_AVAILABLE_DOWNLOAD_BW -> "${stats.estimatedDownloadBandwidth} kbit/s"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.Call
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
|
||||
class StatisticsListViewModel : ViewModel() {
|
||||
val callStatsList = MutableLiveData<ArrayList<CallStatisticsViewModel>>()
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String?
|
||||
) {
|
||||
if (state == Call.State.End || state == Call.State.Error) {
|
||||
for (stat in callStatsList.value.orEmpty()) {
|
||||
if (stat.call == call) {
|
||||
callStatsList.value?.remove(stat)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
val list = arrayListOf<CallStatisticsViewModel>()
|
||||
for (call in coreContext.core.calls) {
|
||||
if (call.state != Call.State.End && call.state != Call.State.Released && call.state != Call.State.Error) {
|
||||
list.add(CallStatisticsViewModel(call))
|
||||
}
|
||||
}
|
||||
callStatsList.value = list
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.StatusViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class StatusViewModel : StatusViewModel() {
|
||||
val callQualityIcon = MutableLiveData<Int>()
|
||||
val callQualityContentDescription = MutableLiveData<Int>()
|
||||
|
||||
val encryptionIcon = MutableLiveData<Int>()
|
||||
val encryptionContentDescription = MutableLiveData<Int>()
|
||||
val encryptionIconVisible = MutableLiveData<Boolean>()
|
||||
|
||||
val showZrtpDialogEvent: MutableLiveData<Event<Call>> by lazy {
|
||||
MutableLiveData<Event<Call>>()
|
||||
}
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onCallStatsUpdated(core: Core, call: Call, stats: CallStats) {
|
||||
updateCallQualityIcon()
|
||||
}
|
||||
|
||||
override fun onCallEncryptionChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
on: Boolean,
|
||||
authenticationToken: String
|
||||
) {
|
||||
if (call.params.mediaEncryption == MediaEncryption.ZRTP && !call.authenticationTokenVerified) {
|
||||
showZrtpDialogEvent.value = Event(call)
|
||||
} else {
|
||||
updateEncryptionInfo(call)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String?
|
||||
) {
|
||||
if (call == core.currentCall) {
|
||||
updateEncryptionInfo(call)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
|
||||
updateCallQualityIcon()
|
||||
|
||||
val currentCall = coreContext.core.currentCall
|
||||
if (currentCall != null) {
|
||||
updateEncryptionInfo(currentCall)
|
||||
|
||||
if (currentCall.params.mediaEncryption == MediaEncryption.ZRTP && !currentCall.authenticationTokenVerified) {
|
||||
showZrtpDialogEvent.value = Event(currentCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun showZrtpDialog() {
|
||||
val currentCall = coreContext.core.currentCall
|
||||
if (currentCall?.params?.mediaEncryption == MediaEncryption.ZRTP) {
|
||||
showZrtpDialogEvent.value = Event(currentCall)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateEncryptionInfo(call: Call) {
|
||||
if (call.dir == Call.Dir.Incoming && call.state == Call.State.IncomingReceived && call.core.isMediaEncryptionMandatory) {
|
||||
// If the incoming call view is displayed while encryption is mandatory,
|
||||
// we can safely show the security_ok icon
|
||||
encryptionIcon.value = R.drawable.security_ok
|
||||
encryptionIconVisible.value = true
|
||||
encryptionContentDescription.value = R.string.content_description_call_secured
|
||||
return
|
||||
}
|
||||
|
||||
when (call.params.mediaEncryption ?: MediaEncryption.None) {
|
||||
MediaEncryption.SRTP, MediaEncryption.DTLS -> {
|
||||
encryptionIcon.value = R.drawable.security_ok
|
||||
encryptionIconVisible.value = true
|
||||
encryptionContentDescription.value = R.string.content_description_call_secured
|
||||
}
|
||||
MediaEncryption.ZRTP -> {
|
||||
encryptionIcon.value = when (call.authenticationTokenVerified) {
|
||||
true -> R.drawable.security_ok
|
||||
else -> R.drawable.security_pending
|
||||
}
|
||||
encryptionContentDescription.value = when (call.authenticationTokenVerified) {
|
||||
true -> R.string.content_description_call_secured
|
||||
else -> R.string.content_description_call_security_pending
|
||||
}
|
||||
encryptionIconVisible.value = true
|
||||
}
|
||||
MediaEncryption.None -> {
|
||||
encryptionIcon.value = R.drawable.security_ko
|
||||
// Do not show unsecure icon if user doesn't want to do call encryption
|
||||
encryptionIconVisible.value = call.core.mediaEncryption != MediaEncryption.None
|
||||
encryptionContentDescription.value = R.string.content_description_call_not_secured
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateCallQualityIcon() {
|
||||
val call = coreContext.core.currentCall
|
||||
val quality = call?.currentQuality ?: 0f
|
||||
callQualityIcon.value = when {
|
||||
quality >= 4 -> R.drawable.call_quality_indicator_4
|
||||
quality >= 3 -> R.drawable.call_quality_indicator_3
|
||||
quality >= 2 -> R.drawable.call_quality_indicator_2
|
||||
quality >= 1 -> R.drawable.call_quality_indicator_1
|
||||
else -> R.drawable.call_quality_indicator_0
|
||||
}
|
||||
callQualityContentDescription.value = when {
|
||||
quality >= 4 -> R.string.content_description_call_quality_4
|
||||
quality >= 3 -> R.string.content_description_call_quality_3
|
||||
quality >= 2 -> R.string.content_description_call_quality_2
|
||||
quality >= 1 -> R.string.content_description_call_quality_1
|
||||
else -> R.string.content_description_call_quality_0
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.View.OnTouchListener
|
||||
import android.widget.LinearLayout
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.call.viewmodels.IncomingCallViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.CallIncomingAnswerDeclineButtonsBinding
|
||||
|
||||
class AnswerDeclineIncomingCallButtons : LinearLayout {
|
||||
private lateinit var binding: CallIncomingAnswerDeclineButtonsBinding
|
||||
private var mBegin = false
|
||||
private var mDeclineX = 0f
|
||||
private var mAnswerX = 0f
|
||||
private var mOldSize = 0f
|
||||
|
||||
private val mAnswerTouchListener = OnTouchListener { view, motionEvent ->
|
||||
val curX: Float
|
||||
|
||||
when (motionEvent.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
binding.declineButton.visibility = View.GONE
|
||||
mAnswerX = motionEvent.x - view.width
|
||||
mBegin = true
|
||||
mOldSize = 0f
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
curX = motionEvent.x - view.width
|
||||
view.scrollBy((mAnswerX - curX).toInt(), view.scrollY)
|
||||
mOldSize -= mAnswerX - curX
|
||||
mAnswerX = curX
|
||||
if (mOldSize < -25) mBegin = false
|
||||
if (curX < (width / 4) - view.width && !mBegin) {
|
||||
binding.viewModel?.answer(true)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
binding.declineButton.visibility = View.VISIBLE
|
||||
view.scrollTo(0, view.scrollY)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
private val mDeclineTouchListener = OnTouchListener { view, motionEvent ->
|
||||
val curX: Float
|
||||
|
||||
when (motionEvent.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
binding.answerButton.visibility = View.GONE
|
||||
mDeclineX = motionEvent.x
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
curX = motionEvent.x
|
||||
view.scrollBy((mDeclineX - curX).toInt(), view.scrollY)
|
||||
mDeclineX = curX
|
||||
if (curX > 3 * width / 4) {
|
||||
binding.viewModel?.decline(true)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
binding.answerButton.visibility = View.VISIBLE
|
||||
view.scrollTo(0, view.scrollY)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(
|
||||
context,
|
||||
attrs
|
||||
) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
fun setViewModel(viewModel: IncomingCallViewModel) {
|
||||
binding.viewModel = viewModel
|
||||
|
||||
updateSlideMode()
|
||||
}
|
||||
|
||||
private fun init(context: Context) {
|
||||
binding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context), R.layout.call_incoming_answer_decline_buttons, this, true
|
||||
)
|
||||
|
||||
updateSlideMode()
|
||||
}
|
||||
|
||||
private fun updateSlideMode() {
|
||||
val slideMode = binding.viewModel?.screenLocked?.value == true
|
||||
Log.i("[Call Incoming Decline Button] Slide mode is $slideMode")
|
||||
if (slideMode) {
|
||||
binding.answerButton.setOnTouchListener(mAnswerTouchListener)
|
||||
binding.declineButton.setOnTouchListener(mDeclineTouchListener)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.views
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.call.viewmodels.CallViewModel
|
||||
import org.linphone.databinding.CallConferenceBinding
|
||||
|
||||
class ConferenceCallView : LinearLayout {
|
||||
private lateinit var binding: CallConferenceBinding
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(
|
||||
context,
|
||||
attrs
|
||||
) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
binding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context), R.layout.call_conference, this, true
|
||||
)
|
||||
}
|
||||
|
||||
fun setViewModel(viewModel: CallViewModel) {
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.callTimer.base =
|
||||
SystemClock.elapsedRealtime() - (1000 * viewModel.call.duration) // Linphone timestamps are in seconds
|
||||
binding.callTimer.start()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.call.views
|
||||
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.call.viewmodels.CallViewModel
|
||||
import org.linphone.databinding.CallPausedBinding
|
||||
|
||||
class PausedCallView : LinearLayout {
|
||||
private lateinit var binding: CallPausedBinding
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(
|
||||
context,
|
||||
attrs
|
||||
) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet,
|
||||
defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr) {
|
||||
init(context)
|
||||
}
|
||||
|
||||
fun init(context: Context) {
|
||||
binding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context), R.layout.call_paused, this, true
|
||||
)
|
||||
}
|
||||
|
||||
fun setViewModel(viewModel: CallViewModel) {
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.callTimer.base =
|
||||
SystemClock.elapsedRealtime() - (1000 * viewModel.call.duration) // Linphone timestamps are in seconds
|
||||
binding.callTimer.start()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.launcher
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class LauncherActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.launcher_activity)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
coreContext.handler.postDelayed({ onReady() }, 500)
|
||||
}
|
||||
|
||||
private fun onReady() {
|
||||
Log.i("[Launcher] Core is ready")
|
||||
val intent = Intent()
|
||||
intent.setClass(this, MainActivity::class.java)
|
||||
|
||||
// Propagate current intent action, type and data
|
||||
if (getIntent() != null) {
|
||||
val extras = getIntent().extras
|
||||
if (extras != null) intent.putExtras(extras)
|
||||
}
|
||||
intent.action = getIntent().action
|
||||
intent.type = getIntent().type
|
||||
intent.data = getIntent().data
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
215
app/src/main/java/org/linphone/activities/main/MainActivity.kt
Normal file
215
app/src/main/java/org/linphone/activities/main/MainActivity.kt
Normal file
|
@ -0,0 +1,215 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.view.Gravity
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.findNavController
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.net.URLDecoder
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.GenericActivity
|
||||
import org.linphone.activities.SnackBarActivity
|
||||
import org.linphone.activities.assistant.AssistantActivity
|
||||
import org.linphone.activities.call.CallActivity
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.MainActivityBinding
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class MainActivity : GenericActivity(), SnackBarActivity {
|
||||
private lateinit var binding: MainActivityBinding
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
private val listener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
if (corePreferences.contactsShortcuts) {
|
||||
Log.i("[Main Activity] Contact(s) updated, update shortcuts")
|
||||
Compatibility.createShortcutsToContacts(this@MainActivity)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = DataBindingUtil.setContentView(this, R.layout.main_activity)
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = ViewModelProvider(this).get(SharedMainViewModel::class.java)
|
||||
binding.viewModel = sharedViewModel
|
||||
|
||||
sharedViewModel.toggleDrawerEvent.observe(this, Observer {
|
||||
it.consume {
|
||||
if (binding.sideMenu.isDrawerOpen(Gravity.LEFT)) {
|
||||
binding.sideMenu.closeDrawer(binding.sideMenuContent, true)
|
||||
} else {
|
||||
binding.sideMenu.openDrawer(binding.sideMenuContent, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.setGoBackToCallClickListener {
|
||||
val intent = Intent(this, CallActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
if (intent != null) handleIntentParams(intent)
|
||||
|
||||
if (coreContext.core.proxyConfigList.isEmpty()) {
|
||||
if (corePreferences.firstStart) {
|
||||
corePreferences.firstStart = false
|
||||
startActivity(Intent(this, AssistantActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
|
||||
if (intent != null) handleIntentParams(intent)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
coreContext.contactsManager.addListener(listener)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.contactsManager.removeListener(listener)
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun showSnackBar(resourceId: Int) {
|
||||
Snackbar.make(binding.coordinator, resourceId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun handleIntentParams(intent: Intent) {
|
||||
when (intent.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
handleSendImage(intent)
|
||||
}
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
handleSendMultipleImages(intent)
|
||||
}
|
||||
Intent.ACTION_VIEW -> {
|
||||
if (intent.type == AppUtils.getString(R.string.linphone_address_mime_type)) {
|
||||
val contactUri = intent.data
|
||||
if (contactUri != null) {
|
||||
val contactId = coreContext.contactsManager.getAndroidContactIdFromUri(contactUri)
|
||||
if (contactId != null) {
|
||||
val deepLink = "linphone-android://contact/view/$contactId"
|
||||
Log.i("[Main Activity] Found contact URI parameter in intent: $contactUri, starting deep link: $deepLink")
|
||||
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Intent.ACTION_DIAL, Intent.ACTION_CALL -> {
|
||||
val uri = intent.data
|
||||
if (uri != null) {
|
||||
Log.i("[Main Activity] Found uri: $uri to call")
|
||||
val stringUri = uri.toString()
|
||||
var addressToCall: String = stringUri
|
||||
try {
|
||||
addressToCall = URLDecoder.decode(stringUri, "UTF-8")
|
||||
} catch (e: UnsupportedEncodingException) {}
|
||||
|
||||
if (addressToCall.startsWith("sip:")) {
|
||||
addressToCall = addressToCall.substring("sip:".length)
|
||||
} else if (addressToCall.startsWith("tel:")) {
|
||||
addressToCall = addressToCall.substring("tel:".length)
|
||||
}
|
||||
|
||||
Log.i("[Main Activity] Starting dialer with pre-filled URI $addressToCall")
|
||||
val args = Bundle()
|
||||
args.putString("URI", addressToCall)
|
||||
findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_dialerFragment, args)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
when {
|
||||
intent.hasExtra("ContactId") -> {
|
||||
val id = intent.getStringExtra("ContactId")
|
||||
val deepLink = "linphone-android://contact/view/$id"
|
||||
Log.i("[Main Activity] Found contact id parameter in intent: $id, starting deep link: $deepLink")
|
||||
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
|
||||
}
|
||||
intent.hasExtra("Chat") -> {
|
||||
Log.i("[Main Activity] Found chat intent extra, go to chat rooms list")
|
||||
findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_masterChatRoomsFragment)
|
||||
}
|
||||
intent.hasExtra("Dialer") -> {
|
||||
Log.i("[Main Activity] Found dialer intent extra, go to dialer")
|
||||
val args = Bundle()
|
||||
args.putBoolean("Transfer", intent.getBooleanExtra("Transfer", false))
|
||||
findNavController(R.id.nav_host_fragment).navigate(R.id.action_global_dialerFragment, args)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendImage(intent: Intent) {
|
||||
(intent.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as? Uri)?.let {
|
||||
val list = arrayListOf<String>()
|
||||
val path = FileUtils.getFilePath(this, it)
|
||||
if (path != null) {
|
||||
list.add(path)
|
||||
Log.i("[Main Activity] Found single file to share: $path")
|
||||
}
|
||||
sharedViewModel.filesToShare.value = list
|
||||
|
||||
val deepLink = "linphone-android://chat/"
|
||||
Log.i("[Main Activity] Starting deep link: $deepLink")
|
||||
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleSendMultipleImages(intent: Intent) {
|
||||
intent.getParcelableArrayListExtra<Parcelable>(Intent.EXTRA_STREAM)?.let {
|
||||
val list = arrayListOf<String>()
|
||||
for (parcelable in it) {
|
||||
val uri = parcelable as Uri
|
||||
val path = FileUtils.getFilePath(this, uri)
|
||||
Log.i("[Main Activity] Found file to share: $path")
|
||||
if (path != null) list.add(path)
|
||||
}
|
||||
sharedViewModel.filesToShare.value = list
|
||||
|
||||
val deepLink = "linphone-android://chat/"
|
||||
Log.i("[Main Activity] Starting deep link: $deepLink")
|
||||
findNavController(R.id.nav_host_fragment).navigate(Uri.parse(deepLink))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.about
|
||||
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.AboutFragmentBinding
|
||||
|
||||
class AboutFragment : Fragment() {
|
||||
private lateinit var binding: AboutFragmentBinding
|
||||
private lateinit var viewModel: AboutViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = AboutFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
viewModel = ViewModelProvider(this).get(AboutViewModel::class.java)
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setBackClickListener { findNavController().popBackStack() }
|
||||
|
||||
binding.setPrivacyPolicyClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_privacy_policy_link))
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
|
||||
binding.setLicenseClickListener {
|
||||
val browserIntent = Intent(
|
||||
Intent.ACTION_VIEW,
|
||||
Uri.parse(getString(R.string.about_license_link))
|
||||
)
|
||||
startActivity(browserIntent)
|
||||
}
|
||||
|
||||
viewModel.uploadFinishedEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { url ->
|
||||
val clipboard =
|
||||
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Logs url", url)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
|
||||
val activity = requireActivity() as MainActivity
|
||||
activity.showSnackBar(R.string.logs_url_copied_to_clipboard)
|
||||
|
||||
shareUploadedLogsUrl(url)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Logs
|
||||
private fun shareUploadedLogsUrl(info: String) {
|
||||
val appName = getString(R.string.app_name)
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.putExtra(
|
||||
Intent.EXTRA_EMAIL,
|
||||
arrayOf(getString(R.string.about_bugreport_email))
|
||||
)
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "$appName Logs")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, info)
|
||||
intent.type = "application/zip"
|
||||
|
||||
try {
|
||||
startActivity(Intent.createChooser(intent, "Send mail..."))
|
||||
} catch (ex: ActivityNotFoundException) {
|
||||
Log.e(ex)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.about
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.Core
|
||||
import org.linphone.core.CoreListenerStub
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class AboutViewModel : ViewModel() {
|
||||
val appVersion: String = coreContext.appVersion
|
||||
|
||||
val sdkVersion: String = coreContext.sdkVersion
|
||||
|
||||
val showLogsButtons: Boolean = corePreferences.debugLogs
|
||||
|
||||
val uploadInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val uploadFinishedEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val listener = object : CoreListenerStub() {
|
||||
override fun onLogCollectionUploadStateChanged(
|
||||
core: Core,
|
||||
state: Core.LogCollectionUploadState,
|
||||
info: String
|
||||
) {
|
||||
if (state == Core.LogCollectionUploadState.Delivered) {
|
||||
uploadInProgress.value = false
|
||||
uploadFinishedEvent.value = Event(info)
|
||||
} else if (state == Core.LogCollectionUploadState.NotDelivered) {
|
||||
uploadInProgress.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
coreContext.core.addListener(listener)
|
||||
uploadInProgress.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun uploadLogs() {
|
||||
uploadInProgress.value = true
|
||||
coreContext.core.uploadLogCollection()
|
||||
}
|
||||
|
||||
fun resetLogs() {
|
||||
coreContext.core.resetLogCollection()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
internal abstract class ChatScrollListener(private val mLayoutManager: LinearLayoutManager) :
|
||||
RecyclerView.OnScrollListener() {
|
||||
// The total number of items in the data set after the last load
|
||||
private var previousTotalItemCount = 0
|
||||
// True if we are still waiting for the last set of data to load.
|
||||
private var loading = true
|
||||
|
||||
// This happens many times a second during a scroll, so be wary of the code you place here.
|
||||
// We are given a few useful parameters to help us work out if we need to load some more data,
|
||||
// but first we check if we are waiting for the previous load to finish.
|
||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||
val totalItemCount = mLayoutManager.itemCount
|
||||
val firstVisibleItemPosition: Int = mLayoutManager.findFirstVisibleItemPosition()
|
||||
val lastVisibleItemPosition: Int = mLayoutManager.findLastVisibleItemPosition()
|
||||
|
||||
// If the total item count is zero and the previous isn't, assume the
|
||||
// list is invalidated and should be reset back to initial state
|
||||
if (totalItemCount < previousTotalItemCount) {
|
||||
previousTotalItemCount = totalItemCount
|
||||
if (totalItemCount == 0) {
|
||||
loading = true
|
||||
}
|
||||
}
|
||||
|
||||
// If it’s still loading, we check to see if the data set count has
|
||||
// changed, if so we conclude it has finished loading and update the current page
|
||||
// number and total item count.
|
||||
if (loading && totalItemCount > previousTotalItemCount) {
|
||||
loading = false
|
||||
previousTotalItemCount = totalItemCount
|
||||
}
|
||||
|
||||
// If it isn’t currently loading, we check to see if we have breached
|
||||
// the mVisibleThreshold and need to reload more data.
|
||||
// If we do need to reload some more data, we execute onLoadMore to fetch the data.
|
||||
// threshold should reflect how many total columns there are too
|
||||
if (!loading && firstVisibleItemPosition < mVisibleThreshold && firstVisibleItemPosition > 0 && lastVisibleItemPosition < totalItemCount - mVisibleThreshold) {
|
||||
onLoadMore(totalItemCount)
|
||||
loading = true
|
||||
}
|
||||
}
|
||||
|
||||
// Defines the process for actually loading more data based on page
|
||||
protected abstract fun onLoadMore(totalItemsCount: Int)
|
||||
|
||||
companion object {
|
||||
// The minimum amount of items to have below your current scroll position
|
||||
// before loading more.
|
||||
private const val mVisibleThreshold = 5
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat
|
||||
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
|
||||
data class GroupChatRoomMember(
|
||||
val address: Address,
|
||||
var isAdmin: Boolean = false,
|
||||
val securityLevel: ChatRoomSecurityLevel = ChatRoomSecurityLevel.ClearText,
|
||||
val hasLimeX3DHCapability: Boolean = false
|
||||
)
|
|
@ -0,0 +1,330 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatMessageViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.EventViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.OnContentClickedListener
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatRoomCapabilities
|
||||
import org.linphone.core.EventLog
|
||||
import org.linphone.databinding.ChatEventListCellBinding
|
||||
import org.linphone.databinding.ChatMessageListCellBinding
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LifecycleListAdapter
|
||||
import org.linphone.utils.LifecycleViewHolder
|
||||
|
||||
class ChatMessagesListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter<EventLog, LifecycleViewHolder>(ChatMessageDiffCallback()) {
|
||||
companion object {
|
||||
const val MAX_TIME_TO_GROUP_MESSAGES = 300 // 5 minutes
|
||||
}
|
||||
|
||||
val resendMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val deleteMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val forwardMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val showImdnForMessageEvent: MutableLiveData<Event<ChatMessage>> by lazy {
|
||||
MutableLiveData<Event<ChatMessage>>()
|
||||
}
|
||||
|
||||
val addSipUriToContactEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val openContentEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
private val contentClickedListener = object : OnContentClickedListener {
|
||||
override fun onContentClicked(path: String) {
|
||||
openContentEvent.value = Event(path)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LifecycleViewHolder {
|
||||
return when (viewType) {
|
||||
EventLog.Type.ConferenceChatMessage.toInt() -> createChatMessageViewHolder(parent)
|
||||
else -> createEventViewHolder(parent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChatMessageViewHolder(parent: ViewGroup): ChatMessageViewHolder {
|
||||
val binding: ChatMessageListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_message_list_cell, parent, false
|
||||
)
|
||||
val viewHolder = ChatMessageViewHolder(binding)
|
||||
binding.lifecycleOwner = viewHolder
|
||||
return viewHolder
|
||||
}
|
||||
|
||||
private fun createEventViewHolder(parent: ViewGroup): EventViewHolder {
|
||||
val binding: ChatEventListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_event_list_cell, parent, false
|
||||
)
|
||||
val viewHolder = EventViewHolder(binding)
|
||||
binding.lifecycleOwner = viewHolder
|
||||
return viewHolder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: LifecycleViewHolder, position: Int) {
|
||||
val eventLog = getItem(position)
|
||||
when (holder) {
|
||||
is ChatMessageViewHolder -> holder.bind(eventLog)
|
||||
is EventViewHolder -> holder.bind(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val eventLog = getItem(position)
|
||||
return eventLog.type.toInt()
|
||||
}
|
||||
|
||||
inner class ChatMessageViewHolder(
|
||||
private val binding: ChatMessageListCellBinding
|
||||
) : LifecycleViewHolder(binding), PopupMenu.OnMenuItemClickListener {
|
||||
fun bind(eventLog: EventLog) {
|
||||
with(binding) {
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
val chatMessageViewModel = ChatMessageViewModel(chatMessage, contentClickedListener)
|
||||
viewModel = chatMessageViewModel
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(this@ChatMessageViewHolder, Observer {
|
||||
position = adapterPosition
|
||||
})
|
||||
|
||||
binding.setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(adapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
// Grouping
|
||||
var hasPrevious = false
|
||||
var hasNext = false
|
||||
|
||||
if (adapterPosition > 0) {
|
||||
val previousItem = getItem(adapterPosition - 1)
|
||||
if (previousItem.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val previousMessage = previousItem.chatMessage
|
||||
if (previousMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
||||
if (chatMessage.time - previousMessage.time < MAX_TIME_TO_GROUP_MESSAGES) {
|
||||
hasPrevious = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adapterPosition >= 0 && adapterPosition < itemCount - 1) {
|
||||
val nextItem = getItem(adapterPosition + 1)
|
||||
if (nextItem.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val nextMessage = nextItem.chatMessage
|
||||
if (nextMessage.fromAddress.weakEqual(chatMessage.fromAddress)) {
|
||||
if (nextMessage.time - chatMessage.time < MAX_TIME_TO_GROUP_MESSAGES) {
|
||||
hasNext = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
chatMessageViewModel.updateBubbleBackground(hasPrevious, hasNext)
|
||||
|
||||
executePendingBindings()
|
||||
|
||||
setContextMenuClickListener {
|
||||
val popup = PopupMenu(root.context, background)
|
||||
popup.setOnMenuItemClickListener(this@ChatMessageViewHolder)
|
||||
popup.inflate(R.menu.chat_message_menu)
|
||||
|
||||
if (!chatMessage.isOutgoing ||
|
||||
chatMessage.chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()) ||
|
||||
chatMessage.state == ChatMessage.State.NotDelivered) { // No message id
|
||||
popup.menu.removeItem(R.id.chat_message_menu_imdn_infos)
|
||||
}
|
||||
if (chatMessage.state != ChatMessage.State.NotDelivered) {
|
||||
popup.menu.removeItem(R.id.chat_message_menu_resend)
|
||||
}
|
||||
if (!chatMessage.hasTextContent()) {
|
||||
popup.menu.removeItem(R.id.chat_message_menu_copy_text)
|
||||
}
|
||||
if (chatMessageViewModel.contact.value != null) {
|
||||
popup.menu.removeItem(R.id.chat_message_menu_add_to_contacts)
|
||||
}
|
||||
|
||||
popup.show()
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.chat_message_menu_imdn_infos -> {
|
||||
showImdnDeliveryFragment()
|
||||
true
|
||||
}
|
||||
R.id.chat_message_menu_resend -> {
|
||||
resendMessage()
|
||||
true
|
||||
}
|
||||
R.id.chat_message_menu_copy_text -> {
|
||||
copyTextToClipboard()
|
||||
true
|
||||
}
|
||||
R.id.chat_message_forward_message -> {
|
||||
forwardMessage()
|
||||
true
|
||||
}
|
||||
R.id.chat_message_menu_delete_message -> {
|
||||
deleteMessage()
|
||||
true
|
||||
}
|
||||
R.id.chat_message_menu_add_to_contacts -> {
|
||||
addSenderToContacts()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun resendMessage() {
|
||||
val chatMessage = binding.viewModel?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
val viewHolder = binding.lifecycleOwner as ChatMessageViewHolder
|
||||
chatMessage.userData = viewHolder.adapterPosition
|
||||
resendMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyTextToClipboard() {
|
||||
val chatMessage = binding.viewModel?.chatMessage
|
||||
if (chatMessage != null && chatMessage.hasTextContent()) {
|
||||
val clipboard: ClipboardManager = coreContext.context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText("Message", chatMessage.textContent)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
}
|
||||
|
||||
private fun forwardMessage() {
|
||||
val chatMessage = binding.viewModel?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
forwardMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showImdnDeliveryFragment() {
|
||||
val chatMessage = binding.viewModel?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
showImdnForMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMessage() {
|
||||
val chatMessage = binding.viewModel?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
val viewHolder = binding.lifecycleOwner as ChatMessageViewHolder
|
||||
chatMessage.userData = viewHolder.adapterPosition
|
||||
deleteMessageEvent.value = Event(chatMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addSenderToContacts() {
|
||||
val chatMessage = binding.viewModel?.chatMessage
|
||||
if (chatMessage != null) {
|
||||
chatMessage.fromAddress.clean() // To remove gruu if any
|
||||
addSipUriToContactEvent.value = Event(chatMessage.fromAddress.asStringUriOnly())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class EventViewHolder(
|
||||
private val binding: ChatEventListCellBinding
|
||||
) : LifecycleViewHolder(binding) {
|
||||
fun bind(eventLog: EventLog) {
|
||||
with(binding) {
|
||||
val eventViewModel = EventViewModel(eventLog)
|
||||
viewModel = eventViewModel
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(this@EventViewHolder, Observer {
|
||||
position = adapterPosition
|
||||
})
|
||||
|
||||
binding.setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(adapterPosition)
|
||||
}
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ChatMessageDiffCallback : DiffUtil.ItemCallback<EventLog>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: EventLog,
|
||||
newItem: EventLog
|
||||
): Boolean {
|
||||
return if (oldItem.type == EventLog.Type.ConferenceChatMessage &&
|
||||
newItem.type == EventLog.Type.ConferenceChatMessage) {
|
||||
oldItem.chatMessage.time == newItem.chatMessage.time &&
|
||||
oldItem.chatMessage.isOutgoing == newItem.chatMessage.isOutgoing
|
||||
} else oldItem.notifyId == newItem.notifyId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: EventLog,
|
||||
newItem: EventLog
|
||||
): Boolean {
|
||||
return if (newItem.type == EventLog.Type.ConferenceChatMessage) {
|
||||
newItem.chatMessage.state == ChatMessage.State.Displayed
|
||||
} else false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationContactViewModel
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.FriendCapability
|
||||
import org.linphone.core.SearchResult
|
||||
import org.linphone.databinding.ChatRoomCreationContactCellBinding
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LifecycleListAdapter
|
||||
import org.linphone.utils.LifecycleViewHolder
|
||||
|
||||
class ChatRoomCreationContactsAdapter : LifecycleListAdapter<SearchResult, ChatRoomCreationContactsAdapter.ViewHolder>(SearchResultDiffCallback()) {
|
||||
val selectedContact = MutableLiveData<Event<SearchResult>>()
|
||||
|
||||
val selectedAddresses = MutableLiveData<ArrayList<Address>>()
|
||||
|
||||
var groupChatEnabled: Boolean = false
|
||||
|
||||
val securityEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomCreationContactCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_creation_contact_cell, parent, false
|
||||
)
|
||||
val viewHolder = ViewHolder(binding)
|
||||
binding.lifecycleOwner = viewHolder
|
||||
return viewHolder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ChatRoomCreationContactCellBinding
|
||||
) : LifecycleViewHolder(binding) {
|
||||
fun bind(searchResult: SearchResult) {
|
||||
with(binding) {
|
||||
val searchResultViewModel = ChatRoomCreationContactViewModel(searchResult)
|
||||
viewModel = searchResultViewModel
|
||||
|
||||
securityEnabled.observe(this@ViewHolder, Observer {
|
||||
updateSecurity(searchResult, searchResultViewModel, it)
|
||||
})
|
||||
|
||||
selectedAddresses.observe(this@ViewHolder, Observer {
|
||||
val selected = it.find { address ->
|
||||
if (searchResult.address != null) address.weakEqual(searchResult.address) else false
|
||||
}
|
||||
searchResultViewModel.isSelected.value = selected != null
|
||||
})
|
||||
|
||||
setClickListener {
|
||||
selectedContact.value = Event(searchResult)
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSecurity(
|
||||
searchResult: SearchResult,
|
||||
viewModel: ChatRoomCreationContactViewModel,
|
||||
securityEnabled: Boolean
|
||||
) {
|
||||
val isMyself = securityEnabled && searchResult.address != null && coreContext.core.defaultProxyConfig?.identityAddress?.weakEqual(searchResult.address) ?: false
|
||||
val limeCheck = !securityEnabled || (securityEnabled && searchResult.hasCapability(FriendCapability.LimeX3Dh))
|
||||
val groupCheck = !groupChatEnabled || (groupChatEnabled && searchResult.hasCapability(FriendCapability.GroupChat))
|
||||
val disabled = if (searchResult.friend != null) !limeCheck || !groupCheck || isMyself else false // Generated entry from search filter
|
||||
|
||||
viewModel.isDisabled.value = disabled
|
||||
|
||||
if (disabled && viewModel.isSelected.value == true) {
|
||||
// Remove item from selection if both selected and disabled
|
||||
selectedContact.postValue(Event(searchResult))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SearchResultDiffCallback : DiffUtil.ItemCallback<SearchResult>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: SearchResult,
|
||||
newItem: SearchResult
|
||||
): Boolean {
|
||||
return if (oldItem.address != null && newItem.address != null) oldItem.address.weakEqual(newItem.address) else false
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: SearchResult,
|
||||
newItem: SearchResult
|
||||
): Boolean {
|
||||
return newItem.friend != null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomViewModel
|
||||
import org.linphone.activities.main.viewmodels.ListTopBarViewModel
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.databinding.ChatRoomListCellBinding
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LifecycleListAdapter
|
||||
import org.linphone.utils.LifecycleViewHolder
|
||||
|
||||
class ChatRoomsListAdapter(val selectionViewModel: ListTopBarViewModel) : LifecycleListAdapter<ChatRoom, ChatRoomsListAdapter.ViewHolder>(ChatRoomDiffCallback()) {
|
||||
val selectedChatRoomEvent: MutableLiveData<Event<ChatRoom>> by lazy {
|
||||
MutableLiveData<Event<ChatRoom>>()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomListCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_list_cell, parent, false
|
||||
)
|
||||
val viewHolder = ViewHolder(binding)
|
||||
binding.lifecycleOwner = viewHolder
|
||||
return viewHolder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ChatRoomListCellBinding
|
||||
) : LifecycleViewHolder(binding) {
|
||||
fun bind(chatRoom: ChatRoom) {
|
||||
with(binding) {
|
||||
val chatRoomViewModel = ChatRoomViewModel(chatRoom)
|
||||
viewModel = chatRoomViewModel
|
||||
|
||||
// This is for item selection through ListTopBarFragment
|
||||
selectionListViewModel = selectionViewModel
|
||||
selectionViewModel.isEditionEnabled.observe(this@ViewHolder, Observer {
|
||||
position = adapterPosition
|
||||
})
|
||||
|
||||
binding.setClickListener {
|
||||
if (selectionViewModel.isEditionEnabled.value == true) {
|
||||
selectionViewModel.onToggleSelect(adapterPosition)
|
||||
} else {
|
||||
selectedChatRoomEvent.value = Event(chatRoom)
|
||||
chatRoom.markAsRead()
|
||||
}
|
||||
}
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ChatRoomDiffCallback : DiffUtil.ItemCallback<ChatRoom>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ChatRoom,
|
||||
newItem: ChatRoom
|
||||
): Boolean {
|
||||
return oldItem.localAddress.weakEqual(newItem.localAddress) &&
|
||||
oldItem.peerAddress.weakEqual(newItem.peerAddress)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ChatRoom,
|
||||
newItem: ChatRoom
|
||||
): Boolean {
|
||||
return newItem.unreadMessagesCount == 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.viewmodels.GroupInfoParticipantViewModel
|
||||
import org.linphone.databinding.ChatRoomGroupInfoParticipantCellBinding
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LifecycleListAdapter
|
||||
import org.linphone.utils.LifecycleViewHolder
|
||||
|
||||
class GroupInfoParticipantsAdapter(private val isEncryptionEnabled: Boolean) : LifecycleListAdapter<GroupChatRoomMember,
|
||||
GroupInfoParticipantsAdapter.ViewHolder>(ParticipantDiffCallback()) {
|
||||
private var showAdmin: Boolean = false
|
||||
|
||||
val participantRemovedEvent: MutableLiveData<Event<GroupChatRoomMember>> by lazy {
|
||||
MutableLiveData<Event<GroupChatRoomMember>>()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomGroupInfoParticipantCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_group_info_participant_cell, parent, false
|
||||
)
|
||||
val viewHolder = ViewHolder(binding)
|
||||
binding.lifecycleOwner = viewHolder
|
||||
return viewHolder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
fun showAdminControls(show: Boolean) {
|
||||
showAdmin = show
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ChatRoomGroupInfoParticipantCellBinding
|
||||
) : LifecycleViewHolder(binding) {
|
||||
fun bind(participant: GroupChatRoomMember) {
|
||||
with(binding) {
|
||||
val participantViewModel = GroupInfoParticipantViewModel(participant)
|
||||
participantViewModel.showAdminControls.value = showAdmin
|
||||
viewModel = participantViewModel
|
||||
|
||||
setRemoveClickListener {
|
||||
participantRemovedEvent.value = Event(participant)
|
||||
}
|
||||
isEncrypted = isEncryptionEnabled
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ParticipantDiffCallback : DiffUtil.ItemCallback<GroupChatRoomMember>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: GroupChatRoomMember,
|
||||
newItem: GroupChatRoomMember
|
||||
): Boolean {
|
||||
return oldItem.address.weakEqual(newItem.address)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: GroupChatRoomMember,
|
||||
newItem: GroupChatRoomMember
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.adapters
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.viewmodels.ImdnParticipantViewModel
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ParticipantImdnState
|
||||
import org.linphone.databinding.ChatRoomImdnParticipantCellBinding
|
||||
import org.linphone.databinding.ImdnListHeaderBinding
|
||||
import org.linphone.utils.HeaderAdapter
|
||||
import org.linphone.utils.LifecycleViewHolder
|
||||
|
||||
class ImdnAdapter : ListAdapter<ParticipantImdnState,
|
||||
ImdnAdapter.ViewHolder>(ParticipantImdnStateDiffCallback()), HeaderAdapter {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val binding: ChatRoomImdnParticipantCellBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
R.layout.chat_room_imdn_participant_cell, parent, false
|
||||
)
|
||||
val viewHolder = ViewHolder(binding)
|
||||
binding.lifecycleOwner = viewHolder
|
||||
return viewHolder
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
inner class ViewHolder(
|
||||
private val binding: ChatRoomImdnParticipantCellBinding
|
||||
) : LifecycleViewHolder(binding) {
|
||||
fun bind(participantImdnState: ParticipantImdnState) {
|
||||
with(binding) {
|
||||
val imdnViewModel = ImdnParticipantViewModel(participantImdnState)
|
||||
viewModel = imdnViewModel
|
||||
|
||||
executePendingBindings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun displayHeaderForPosition(position: Int): Boolean {
|
||||
if (position >= itemCount) return false
|
||||
val participantImdnState = getItem(position)
|
||||
val previousPosition = position - 1
|
||||
return if (previousPosition >= 0) {
|
||||
getItem(previousPosition).state != participantImdnState.state
|
||||
} else true
|
||||
}
|
||||
|
||||
override fun getHeaderViewForPosition(context: Context, position: Int): View {
|
||||
val participantImdnState = getItem(position)
|
||||
val binding: ImdnListHeaderBinding = DataBindingUtil.inflate(
|
||||
LayoutInflater.from(context),
|
||||
R.layout.imdn_list_header, null, false
|
||||
)
|
||||
when (participantImdnState.state) {
|
||||
ChatMessage.State.Displayed -> {
|
||||
binding.title = R.string.chat_message_imdn_displayed
|
||||
binding.textColor = R.color.imdn_read_color
|
||||
binding.icon = R.drawable.message_read
|
||||
}
|
||||
ChatMessage.State.DeliveredToUser -> {
|
||||
binding.title = R.string.chat_message_imdn_delivered
|
||||
binding.textColor = R.color.grey_color
|
||||
binding.icon = R.drawable.message_delivered
|
||||
}
|
||||
ChatMessage.State.Delivered -> {
|
||||
binding.title = R.string.chat_message_imdn_sent
|
||||
binding.textColor = R.color.grey_color
|
||||
binding.icon = R.drawable.message_delivered
|
||||
}
|
||||
ChatMessage.State.NotDelivered -> {
|
||||
binding.title = R.string.chat_message_imdn_undelivered
|
||||
binding.textColor = R.color.red_color
|
||||
binding.icon = R.drawable.message_undelivered
|
||||
}
|
||||
}
|
||||
binding.executePendingBindings()
|
||||
return binding.root
|
||||
}
|
||||
}
|
||||
|
||||
private class ParticipantImdnStateDiffCallback : DiffUtil.ItemCallback<ParticipantImdnState>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: ParticipantImdnState,
|
||||
newItem: ParticipantImdnState
|
||||
): Boolean {
|
||||
return oldItem.participant.address.weakEqual(newItem.participant.address)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ParticipantImdnState,
|
||||
newItem: ParticipantImdnState
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.SearchView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.adapters.ChatRoomCreationContactsAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomCreationViewModel
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.databinding.ChatRoomCreationFragmentBinding
|
||||
|
||||
class ChatRoomCreationFragment : Fragment() {
|
||||
private lateinit var binding: ChatRoomCreationFragmentBinding
|
||||
private lateinit var viewModel: ChatRoomCreationViewModel
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
private lateinit var adapter: ChatRoomCreationContactsAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = ChatRoomCreationFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedMainViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
val createGroup = arguments?.getBoolean("createGroup") ?: false
|
||||
|
||||
viewModel = ViewModelProvider(this).get(ChatRoomCreationViewModel::class.java)
|
||||
viewModel.createGroupChat.value = createGroup
|
||||
|
||||
viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
|
||||
|
||||
binding.viewModel = viewModel
|
||||
|
||||
adapter = ChatRoomCreationContactsAdapter()
|
||||
adapter.groupChatEnabled = viewModel.createGroupChat.value == true
|
||||
adapter.securityEnabled.value = viewModel.isEncrypted.value == true
|
||||
binding.contactsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(activity)
|
||||
binding.contactsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation)
|
||||
dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null))
|
||||
binding.contactsList.addItemDecoration(dividerItemDecoration)
|
||||
|
||||
binding.setBackClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
binding.setAllContactsToggleClickListener {
|
||||
viewModel.sipContactsSelected.value = false
|
||||
}
|
||||
|
||||
binding.setSipContactsToggleClickListener {
|
||||
viewModel.sipContactsSelected.value = true
|
||||
}
|
||||
|
||||
viewModel.contactsList.observe(viewLifecycleOwner, Observer {
|
||||
adapter.submitList(it)
|
||||
})
|
||||
|
||||
viewModel.isEncrypted.observe(viewLifecycleOwner, Observer {
|
||||
adapter.securityEnabled.value = it
|
||||
})
|
||||
|
||||
viewModel.sipContactsSelected.observe(viewLifecycleOwner, Observer {
|
||||
viewModel.updateContactsList()
|
||||
})
|
||||
|
||||
viewModel.selectedAddresses.observe(viewLifecycleOwner, Observer {
|
||||
adapter.selectedAddresses.value = it
|
||||
})
|
||||
|
||||
viewModel.chatRoomCreatedEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { chatRoom ->
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) {
|
||||
findNavController().navigate(R.id.action_chatRoomCreationFragment_to_detailChatRoomFragment)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
adapter.selectedContact.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { searchResult ->
|
||||
if (createGroup) {
|
||||
viewModel.toggleSelectionForSearchResult(searchResult)
|
||||
} else {
|
||||
viewModel.createOneToOneChat(searchResult)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
addParticipantsFromBundle()
|
||||
|
||||
// Next button is only used to go to group chat info fragment
|
||||
binding.setNextClickListener {
|
||||
sharedViewModel.createEncryptedChatRoom = viewModel.isEncrypted.value == true
|
||||
|
||||
if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) {
|
||||
val args = Bundle()
|
||||
args.putSerializable("participants", viewModel.selectedAddresses.value)
|
||||
findNavController().navigate(R.id.action_chatRoomCreationFragment_to_groupInfoFragment, args)
|
||||
}
|
||||
}
|
||||
|
||||
binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
viewModel.filter(newText ?: "")
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
viewModel.onErrorEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun addParticipantsFromBundle() {
|
||||
val participants = arguments?.getSerializable("participants") as? ArrayList<Address>
|
||||
if (participants != null && participants.size > 0) {
|
||||
viewModel.selectedAddresses.value = participants
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,565 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.provider.MediaStore
|
||||
import android.view.*
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.appcompat.view.menu.MenuBuilder
|
||||
import androidx.appcompat.view.menu.MenuPopupHelper
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import java.io.File
|
||||
import kotlinx.android.synthetic.main.tabs_fragment.*
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.ChatScrollListener
|
||||
import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.*
|
||||
import org.linphone.activities.main.fragments.MasterFragment
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomDetailFragmentBinding
|
||||
import org.linphone.utils.*
|
||||
import org.linphone.utils.Event
|
||||
|
||||
class DetailChatRoomFragment : MasterFragment() {
|
||||
private lateinit var binding: ChatRoomDetailFragmentBinding
|
||||
private lateinit var viewModel: ChatRoomViewModel
|
||||
private lateinit var chatSendingViewModel: ChatMessageSendingViewModel
|
||||
private lateinit var listViewModel: ChatMessagesListViewModel
|
||||
private lateinit var adapter: ChatMessagesListAdapter
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
private var chatRoomAddress: String? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = ChatRoomDetailFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedMainViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
chatRoom ?: return
|
||||
chatRoomAddress = chatRoom.peerAddress.asStringUriOnly()
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatRoomViewModelFactory(chatRoom)
|
||||
)[ChatRoomViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
chatSendingViewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatMessageSendingViewModelFactory(chatRoom)
|
||||
)[ChatMessageSendingViewModel::class.java]
|
||||
binding.chatSendingViewModel = chatSendingViewModel
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this,
|
||||
ChatMessagesListViewModelFactory(chatRoom)
|
||||
)[ChatMessagesListViewModel::class.java]
|
||||
|
||||
adapter = ChatMessagesListAdapter(listSelectionViewModel)
|
||||
// SubmitList is done on a background thread
|
||||
// We need this adapter data observer to know when to scroll
|
||||
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
if (positionStart == adapter.itemCount - 1) {
|
||||
adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
|
||||
scrollToBottom()
|
||||
}
|
||||
}
|
||||
})
|
||||
binding.chatMessagesList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(activity)
|
||||
layoutManager.stackFromEnd = true
|
||||
binding.chatMessagesList.layoutManager = layoutManager
|
||||
|
||||
val chatScrollListener: ChatScrollListener = object : ChatScrollListener(layoutManager) {
|
||||
override fun onLoadMore(totalItemsCount: Int) {
|
||||
listViewModel.loadMoreData(totalItemsCount)
|
||||
}
|
||||
}
|
||||
binding.chatMessagesList.addOnScrollListener(chatScrollListener)
|
||||
|
||||
listViewModel.events.observe(viewLifecycleOwner, Observer { events ->
|
||||
adapter.submitList(events)
|
||||
})
|
||||
|
||||
listViewModel.messageUpdatedEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { position ->
|
||||
adapter.notifyItemChanged(position)
|
||||
}
|
||||
})
|
||||
|
||||
listViewModel.requestWriteExternalStoragePermissionEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
requestPermissions(arrayOf(android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
|
||||
}
|
||||
})
|
||||
|
||||
adapter.deleteMessageEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { chatMessage ->
|
||||
listViewModel.deleteMessage(chatMessage)
|
||||
}
|
||||
})
|
||||
|
||||
adapter.resendMessageEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { chatMessage ->
|
||||
listViewModel.resendMessage(chatMessage)
|
||||
}
|
||||
})
|
||||
|
||||
adapter.forwardMessageEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { chatMessage ->
|
||||
// Remove observer before setting the message to forward
|
||||
// as we don't want to forward it in this chat room
|
||||
sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner)
|
||||
sharedViewModel.messageToForwardEvent.value = Event(chatMessage)
|
||||
|
||||
val deepLink = "linphone-android://chat/"
|
||||
Log.i("[Chat Room] Forwarding message, starting deep link: $deepLink")
|
||||
findNavController().navigate(Uri.parse(deepLink))
|
||||
}
|
||||
})
|
||||
|
||||
adapter.showImdnForMessageEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { chatMessage ->
|
||||
val args = Bundle()
|
||||
args.putString("MessageId", chatMessage.messageId)
|
||||
Navigation.findNavController(binding.root).navigate(R.id.action_detailChatRoomFragment_to_imdnFragment, args)
|
||||
}
|
||||
})
|
||||
|
||||
adapter.addSipUriToContactEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { sipUri ->
|
||||
val deepLink = "linphone-android://contact/new/$sipUri"
|
||||
Log.i("[Chat Room] Creating contact, starting deep link: $deepLink")
|
||||
findNavController().navigate(Uri.parse(deepLink))
|
||||
}
|
||||
})
|
||||
|
||||
adapter.openContentEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { path ->
|
||||
openFile(path)
|
||||
}
|
||||
})
|
||||
|
||||
binding.setBackClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
binding.back.visibility = if (resources.getBoolean(R.bool.isTablet)) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
binding.setTitleClickListener {
|
||||
binding.sipUri.visibility = if (!viewModel.oneToOneChatRoom ||
|
||||
binding.sipUri.visibility == View.VISIBLE) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
binding.setMenuClickListener {
|
||||
showPopupMenu(chatRoom)
|
||||
}
|
||||
|
||||
binding.setEditClickListener {
|
||||
enterEditionMode()
|
||||
}
|
||||
|
||||
binding.setSecurityIconClickListener {
|
||||
showParticipantsDevices()
|
||||
}
|
||||
|
||||
binding.setAttachFileClickListener {
|
||||
if (PermissionHelper.get().hasReadExternalStorage() && PermissionHelper.get().hasCameraPermission()) {
|
||||
pickFile()
|
||||
} else {
|
||||
Log.i("[Chat Room] Asking for READ_EXTERNAL_STORAGE and CAMERA permissions")
|
||||
requestPermissions(arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE, android.Manifest.permission.CAMERA), 0)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setSendMessageClickListener {
|
||||
chatSendingViewModel.sendMessage()
|
||||
binding.message.text?.clear()
|
||||
}
|
||||
|
||||
binding.setStartCallClickListener {
|
||||
coreContext.startCall(viewModel.addressToCall)
|
||||
}
|
||||
|
||||
sharedViewModel.filesToShare.observe(viewLifecycleOwner, Observer {
|
||||
if (it.isNotEmpty()) {
|
||||
for (path in it) {
|
||||
Log.i("[Chat Room] Found $path file to share")
|
||||
chatSendingViewModel.addAttachment(path)
|
||||
}
|
||||
sharedViewModel.filesToShare.value = arrayListOf()
|
||||
}
|
||||
})
|
||||
|
||||
sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { chatMessage ->
|
||||
Log.i("[Chat Room] Found message to transfer")
|
||||
showForwardConfirmationDialog(chatMessage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return adapter.itemCount
|
||||
}
|
||||
|
||||
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
|
||||
val list = ArrayList<EventLog>()
|
||||
for (index in indexesOfItemToDelete) {
|
||||
val eventLog = adapter.getItemAt(index)
|
||||
list.add(eventLog)
|
||||
}
|
||||
listViewModel.deleteEventLogs(list)
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
if (requestCode == 0) {
|
||||
var atLeastOneGranted = false
|
||||
for (result in grantResults) {
|
||||
atLeastOneGranted = atLeastOneGranted || result == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
if (atLeastOneGranted) {
|
||||
pickFile()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
// Prevent notifications for this chat room to be displayed
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = chatRoomAddress
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
coreContext.notificationsManager.currentlyDisplayedChatRoomAddress = null
|
||||
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
var fileToUploadPath: String? = null
|
||||
|
||||
val temporaryFileUploadPath = chatSendingViewModel.temporaryFileUploadPath
|
||||
if (temporaryFileUploadPath != null) {
|
||||
if (data != null) {
|
||||
val dataUri = data.data
|
||||
if (dataUri != null) {
|
||||
fileToUploadPath = dataUri.toString()
|
||||
Log.i("[Chat Room] Using data URI $fileToUploadPath")
|
||||
} else if (temporaryFileUploadPath.exists()) {
|
||||
fileToUploadPath = temporaryFileUploadPath.absolutePath
|
||||
Log.i("[Chat Room] Data URI is null, using $fileToUploadPath")
|
||||
}
|
||||
} else if (temporaryFileUploadPath.exists()) {
|
||||
fileToUploadPath = temporaryFileUploadPath.absolutePath
|
||||
Log.i("[Chat Room] Data is null, using $fileToUploadPath")
|
||||
}
|
||||
}
|
||||
|
||||
if (fileToUploadPath != null) {
|
||||
if (fileToUploadPath.startsWith("content://") ||
|
||||
fileToUploadPath.startsWith("file://")
|
||||
) {
|
||||
val uriToParse = Uri.parse(fileToUploadPath)
|
||||
fileToUploadPath = FileUtils.getFilePath(requireContext(), uriToParse)
|
||||
Log.i("[Chat] Path was using a content or file scheme, real path is: $fileToUploadPath")
|
||||
if (fileToUploadPath == null) {
|
||||
Log.e("[Chat] Failed to get access to file $uriToParse")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileToUploadPath != null) {
|
||||
chatSendingViewModel.addAttachment(fileToUploadPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun enterEditionMode() {
|
||||
listSelectionViewModel.isEditionEnabled.value = true
|
||||
}
|
||||
|
||||
private fun showParticipantsDevices() {
|
||||
if (corePreferences.limeSecurityPopupEnabled) {
|
||||
val dialogViewModel = DialogViewModel(getString(R.string.dialog_lime_security_message))
|
||||
dialogViewModel.showDoNotAskAgain = true
|
||||
val dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showCancelButton { doNotAskAgain ->
|
||||
if (doNotAskAgain) corePreferences.limeSecurityPopupEnabled = false
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
val okLabel = if (viewModel.oneParticipantOneDevice) getString(R.string.dialog_call) else getString(R.string.dialog_ok)
|
||||
dialogViewModel.showOkButton({ doNotAskAgain ->
|
||||
if (doNotAskAgain) corePreferences.limeSecurityPopupEnabled = false
|
||||
|
||||
if (viewModel.oneParticipantOneDevice) {
|
||||
coreContext.startCall(viewModel.onlyParticipantOnlyDeviceAddress, true)
|
||||
} else {
|
||||
if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
|
||||
findNavController().navigate(R.id.action_detailChatRoomFragment_to_devicesFragment)
|
||||
}
|
||||
}
|
||||
|
||||
dialog.dismiss()
|
||||
}, okLabel)
|
||||
|
||||
dialog.show()
|
||||
} else {
|
||||
if (viewModel.oneParticipantOneDevice) {
|
||||
coreContext.startCall(viewModel.onlyParticipantOnlyDeviceAddress, true)
|
||||
} else {
|
||||
if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
|
||||
findNavController().navigate(R.id.action_detailChatRoomFragment_to_devicesFragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showGroupInfo(chatRoom: ChatRoom) {
|
||||
sharedViewModel.selectedGroupChatRoom.value = chatRoom
|
||||
if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
|
||||
findNavController().navigate(R.id.action_detailChatRoomFragment_to_groupInfoFragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEphemeralMessages() {
|
||||
if (findNavController().currentDestination?.id == R.id.detailChatRoomFragment) {
|
||||
findNavController().navigate(R.id.action_detailChatRoomFragment_to_ephemeralFragment)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showForwardConfirmationDialog(chatMessage: ChatMessage) {
|
||||
val viewModel = DialogViewModel(getString(R.string.chat_message_forward_confirmation_dialog))
|
||||
viewModel.iconResource = R.drawable.forward_message_dialog_default
|
||||
viewModel.showIcon = true
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||
|
||||
viewModel.showCancelButton {
|
||||
Log.i("[Chat Room] Transfer cancelled")
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
viewModel.showOkButton({
|
||||
Log.i("[Chat Room] Transfer confirmed")
|
||||
chatSendingViewModel.transferMessage(chatMessage)
|
||||
dialog.dismiss()
|
||||
}, getString(R.string.chat_message_context_menu_forward))
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun showPopupMenu(chatRoom: ChatRoom) {
|
||||
val builder = MenuBuilder(requireContext())
|
||||
val popupMenu = MenuPopupHelper(requireContext(), builder, binding.menu)
|
||||
popupMenu.setForceShowIcon(true)
|
||||
|
||||
MenuInflater(requireContext()).inflate(R.menu.chat_room_menu, builder)
|
||||
if (viewModel.oneToOneChatRoom) {
|
||||
builder.removeItem(R.id.chat_room_group_info)
|
||||
|
||||
// If one participant one device, a click on security badge
|
||||
// will directly start a call or show the dialog, so don't show this menu
|
||||
if (viewModel.oneParticipantOneDevice) {
|
||||
builder.removeItem(R.id.chat_room_participants_devices)
|
||||
}
|
||||
}
|
||||
if (!viewModel.encryptedChatRoom) {
|
||||
builder.removeItem(R.id.chat_room_participants_devices)
|
||||
builder.removeItem(R.id.chat_room_ephemeral_messages)
|
||||
}
|
||||
// TODO: hide ephemeral menu if not all participants support the feature
|
||||
|
||||
builder.setCallback(object : MenuBuilder.Callback {
|
||||
override fun onMenuModeChange(menu: MenuBuilder?) {}
|
||||
|
||||
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.chat_room_group_info -> {
|
||||
showGroupInfo(chatRoom)
|
||||
true
|
||||
}
|
||||
R.id.chat_room_participants_devices -> {
|
||||
showParticipantsDevices()
|
||||
true
|
||||
}
|
||||
R.id.chat_room_ephemeral_messages -> {
|
||||
showEphemeralMessages()
|
||||
true
|
||||
}
|
||||
R.id.chat_room_delete_messages -> {
|
||||
enterEditionMode()
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
private fun scrollToBottom() {
|
||||
if (adapter.itemCount > 0) {
|
||||
binding.chatMessagesList.scrollToPosition(adapter.itemCount - 1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pickFile() {
|
||||
val cameraIntents = ArrayList<Intent>()
|
||||
|
||||
// Handles image & video picking
|
||||
val galleryIntent = Intent(Intent.ACTION_PICK)
|
||||
galleryIntent.type = "*/*"
|
||||
galleryIntent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
|
||||
|
||||
if (PermissionHelper.get().hasCameraPermission()) {
|
||||
// Allows to capture directly from the camera
|
||||
val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
||||
val tempFileName = System.currentTimeMillis().toString() + ".jpeg"
|
||||
chatSendingViewModel.temporaryFileUploadPath =
|
||||
FileUtils.getFileStoragePath(tempFileName)
|
||||
val uri = Uri.fromFile(chatSendingViewModel.temporaryFileUploadPath)
|
||||
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
|
||||
cameraIntents.add(captureIntent)
|
||||
}
|
||||
|
||||
if (PermissionHelper.get().hasReadExternalStorage()) {
|
||||
// Finally allow any kind of file
|
||||
val fileIntent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
fileIntent.type = "*/*"
|
||||
cameraIntents.add(fileIntent)
|
||||
}
|
||||
|
||||
val chooserIntent =
|
||||
Intent.createChooser(galleryIntent, getString(R.string.chat_message_pick_file_dialog))
|
||||
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, cameraIntents.toArray(arrayOf<Parcelable>()))
|
||||
|
||||
startActivityForResult(chooserIntent, 0)
|
||||
}
|
||||
|
||||
private fun openFile(contentFilePath: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
val contentUri: Uri
|
||||
var path = contentFilePath
|
||||
|
||||
when {
|
||||
path.startsWith("file://") -> {
|
||||
path = path.substring("file://".length)
|
||||
val file = File(path)
|
||||
contentUri = FileProvider.getUriForFile(
|
||||
requireContext(),
|
||||
getString(R.string.file_provider),
|
||||
file
|
||||
)
|
||||
}
|
||||
path.startsWith("content://") -> {
|
||||
contentUri = Uri.parse(path)
|
||||
}
|
||||
else -> {
|
||||
val file = File(path)
|
||||
contentUri = try {
|
||||
FileProvider.getUriForFile(
|
||||
requireContext(),
|
||||
getString(R.string.file_provider),
|
||||
file
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e(
|
||||
"[Chat Message] Couldn't get URI for file $file using file provider ${getString(R.string.file_provider)}"
|
||||
)
|
||||
Uri.parse(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val filePath: String = contentUri.toString()
|
||||
Log.i("[Chat Message] Trying to open file: $filePath")
|
||||
var type: String? = null
|
||||
val extension = FileUtils.getExtensionFromFileName(filePath)
|
||||
|
||||
if (extension.isNotEmpty()) {
|
||||
Log.i("[Chat Message] Found extension $extension")
|
||||
type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
} else {
|
||||
Log.e("[Chat Message] Couldn't find extension")
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
Log.i("[Chat Message] Found matching MIME type $type")
|
||||
} else {
|
||||
type = FileUtils.getMimeFromFile(filePath)
|
||||
Log.e("[Chat Message] Can't get MIME type from extension: $extension, will use $type")
|
||||
}
|
||||
|
||||
intent.setDataAndType(contentUri, type)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
||||
try {
|
||||
startActivity(intent)
|
||||
} catch (anfe: ActivityNotFoundException) {
|
||||
Log.e("[Chat Message] Couldn't find an activity to handle MIME type: $type")
|
||||
val activity = requireActivity() as MainActivity
|
||||
activity.showSnackBar(R.string.chat_room_cant_open_file_no_app_found)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.activities.main.chat.viewmodels.DevicesListViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.DevicesListViewModelFactory
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.databinding.ChatRoomDevicesFragmentBinding
|
||||
|
||||
class DevicesFragment : Fragment() {
|
||||
private lateinit var binding: ChatRoomDevicesFragmentBinding
|
||||
private lateinit var listViewModel: DevicesListViewModel
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = ChatRoomDevicesFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedMainViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
chatRoom ?: return
|
||||
|
||||
listViewModel = ViewModelProvider(
|
||||
this,
|
||||
DevicesListViewModelFactory(chatRoom)
|
||||
)[DevicesListViewModel::class.java]
|
||||
binding.viewModel = listViewModel
|
||||
|
||||
binding.setBackClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.linphone.activities.main.chat.viewmodels.EphemeralViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.EphemeralViewModelFactory
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.databinding.ChatRoomEphemeralFragmentBinding
|
||||
|
||||
class EphemeralFragment : Fragment() {
|
||||
private lateinit var binding: ChatRoomEphemeralFragmentBinding
|
||||
private lateinit var viewModel: EphemeralViewModel
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = ChatRoomEphemeralFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedMainViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
chatRoom ?: return
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
EphemeralViewModelFactory(chatRoom)
|
||||
)[EphemeralViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
binding.setBackClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
binding.setValidClickListener {
|
||||
viewModel.updateChatRoomEphemeralDuration()
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.GroupChatRoomMember
|
||||
import org.linphone.activities.main.chat.adapters.GroupInfoParticipantsAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.GroupInfoViewModelFactory
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomCapabilities
|
||||
import org.linphone.databinding.ChatRoomGroupInfoFragmentBinding
|
||||
import org.linphone.utils.DialogUtils
|
||||
|
||||
class GroupInfoFragment : Fragment() {
|
||||
private lateinit var binding: ChatRoomGroupInfoFragmentBinding
|
||||
private lateinit var viewModel: GroupInfoViewModel
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
private lateinit var adapter: GroupInfoParticipantsAdapter
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = ChatRoomGroupInfoFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedMainViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
val chatRoom: ChatRoom? = sharedViewModel.selectedGroupChatRoom.value
|
||||
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
GroupInfoViewModelFactory(chatRoom)
|
||||
)[GroupInfoViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
|
||||
viewModel.isEncrypted.value = sharedViewModel.createEncryptedChatRoom
|
||||
|
||||
adapter = GroupInfoParticipantsAdapter(chatRoom?.hasCapability(ChatRoomCapabilities.Encrypted.toInt()) ?: viewModel.isEncrypted.value == true)
|
||||
binding.participants.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(activity)
|
||||
binding.participants.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation)
|
||||
dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null))
|
||||
binding.participants.addItemDecoration(dividerItemDecoration)
|
||||
|
||||
viewModel.participants.observe(viewLifecycleOwner, Observer {
|
||||
adapter.submitList(it)
|
||||
})
|
||||
|
||||
viewModel.isMeAdmin.observe(viewLifecycleOwner, Observer {
|
||||
adapter.showAdminControls(it && chatRoom != null)
|
||||
})
|
||||
|
||||
adapter.participantRemovedEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { participant ->
|
||||
viewModel.removeParticipant(participant)
|
||||
}
|
||||
})
|
||||
|
||||
addParticipantsFromBundle()
|
||||
|
||||
binding.setBackClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
|
||||
viewModel.createdChatRoomEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { chatRoom ->
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
goToChatRoom()
|
||||
}
|
||||
})
|
||||
|
||||
binding.setNextClickListener {
|
||||
if (viewModel.chatRoom != null) {
|
||||
viewModel.updateRoom()
|
||||
} else {
|
||||
viewModel.createChatRoom()
|
||||
}
|
||||
}
|
||||
|
||||
binding.setParticipantsClickListener {
|
||||
sharedViewModel.createEncryptedChatRoom = viewModel.isEncrypted.value == true
|
||||
|
||||
if (findNavController().currentDestination?.id == R.id.groupInfoFragment) {
|
||||
val args = Bundle()
|
||||
args.putBoolean("createGroup", true)
|
||||
|
||||
val list = arrayListOf<Address>()
|
||||
for (participant in viewModel.participants.value.orEmpty()) {
|
||||
list.add(participant.address)
|
||||
}
|
||||
args.putSerializable("participants", list)
|
||||
|
||||
findNavController().navigate(R.id.action_groupInfoFragment_to_chatRoomCreationFragment, args)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setLeaveClickListener {
|
||||
val dialogViewModel = DialogViewModel(getString(R.string.chat_room_group_info_leave_dialog_message))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), dialogViewModel)
|
||||
|
||||
dialogViewModel.showDeleteButton({
|
||||
viewModel.leaveGroup()
|
||||
dialog.dismiss()
|
||||
}, getString(R.string.chat_room_group_info_leave_dialog_button))
|
||||
|
||||
dialogViewModel.showCancelButton {
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
viewModel.onErrorEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun addParticipantsFromBundle() {
|
||||
val participants = arguments?.getSerializable("participants") as? ArrayList<Address>
|
||||
if (participants != null && participants.size > 0) {
|
||||
val list = arrayListOf<GroupChatRoomMember>()
|
||||
|
||||
for (address in participants) {
|
||||
val exists = viewModel.participants.value?.find {
|
||||
it.address.weakEqual(address)
|
||||
}
|
||||
|
||||
if (exists != null) {
|
||||
list.add(exists)
|
||||
} else {
|
||||
list.add(GroupChatRoomMember(address, false, hasLimeX3DHCapability = viewModel.isEncrypted.value == true))
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.participants.value = list
|
||||
}
|
||||
}
|
||||
|
||||
private fun goToChatRoom() {
|
||||
if (findNavController().currentDestination?.id == R.id.groupInfoFragment) {
|
||||
findNavController().navigate(R.id.action_groupInfoFragment_to_detailChatRoomFragment)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.chat.adapters.ImdnAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ImdnViewModel
|
||||
import org.linphone.activities.main.chat.viewmodels.ImdnViewModelFactory
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomImdnFragmentBinding
|
||||
import org.linphone.utils.RecyclerViewHeaderDecoration
|
||||
|
||||
class ImdnFragment : Fragment() {
|
||||
private lateinit var binding: ChatRoomImdnFragmentBinding
|
||||
private lateinit var viewModel: ImdnViewModel
|
||||
private lateinit var adapter: ImdnAdapter
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = ChatRoomImdnFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedMainViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
val chatRoom = sharedViewModel.selectedChatRoom.value
|
||||
chatRoom ?: return
|
||||
|
||||
if (arguments != null) {
|
||||
val messageId = arguments?.getString("MessageId")
|
||||
val message = chatRoom.findMessage(messageId)
|
||||
if (message != null) {
|
||||
Log.i("[IMDN] Found message $message with id $messageId")
|
||||
viewModel = ViewModelProvider(
|
||||
this,
|
||||
ImdnViewModelFactory(message)
|
||||
)[ImdnViewModel::class.java]
|
||||
binding.viewModel = viewModel
|
||||
} else {
|
||||
Log.e("[IMDN] Couldn't find message with id $messageId in chat room $chatRoom")
|
||||
findNavController().popBackStack()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
Log.e("[IMDN] Couldn't find message id in intent arguments")
|
||||
findNavController().popBackStack()
|
||||
return
|
||||
}
|
||||
|
||||
adapter = ImdnAdapter()
|
||||
binding.participantsList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(activity)
|
||||
binding.participantsList.layoutManager = layoutManager
|
||||
|
||||
// Divider between items
|
||||
val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation)
|
||||
dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null))
|
||||
binding.participantsList.addItemDecoration(dividerItemDecoration)
|
||||
|
||||
// Displays state header
|
||||
val headerItemDecoration = RecyclerViewHeaderDecoration(adapter)
|
||||
binding.participantsList.addItemDecoration(headerItemDecoration)
|
||||
|
||||
viewModel.participants.observe(viewLifecycleOwner, Observer {
|
||||
adapter.submitList(it)
|
||||
})
|
||||
|
||||
binding.setBackClickListener {
|
||||
findNavController().popBackStack()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,250 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.fragments
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.MainActivity
|
||||
import org.linphone.activities.main.chat.adapters.ChatRoomsListAdapter
|
||||
import org.linphone.activities.main.chat.viewmodels.ChatRoomsListViewModel
|
||||
import org.linphone.activities.main.fragments.MasterFragment
|
||||
import org.linphone.activities.main.viewmodels.DialogViewModel
|
||||
import org.linphone.activities.main.viewmodels.SharedMainViewModel
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.databinding.ChatRoomMasterFragmentBinding
|
||||
import org.linphone.utils.*
|
||||
|
||||
class MasterChatRoomsFragment : MasterFragment() {
|
||||
private lateinit var binding: ChatRoomMasterFragmentBinding
|
||||
private lateinit var listViewModel: ChatRoomsListViewModel
|
||||
private lateinit var adapter: ChatRoomsListAdapter
|
||||
private lateinit var sharedViewModel: SharedMainViewModel
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = ChatRoomMasterFragmentBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
listViewModel = ViewModelProvider(this).get(ChatRoomsListViewModel::class.java)
|
||||
binding.viewModel = listViewModel
|
||||
|
||||
sharedViewModel = activity?.run {
|
||||
ViewModelProvider(this).get(SharedMainViewModel::class.java)
|
||||
} ?: throw Exception("Invalid Activity")
|
||||
|
||||
adapter = ChatRoomsListAdapter(listSelectionViewModel)
|
||||
// SubmitList is done on a background thread
|
||||
// We need this adapter data observer to know when to scroll
|
||||
adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
scrollToTop()
|
||||
}
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
scrollToTop()
|
||||
}
|
||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
||||
scrollToTop()
|
||||
}
|
||||
})
|
||||
binding.chatList.adapter = adapter
|
||||
|
||||
val layoutManager = LinearLayoutManager(activity)
|
||||
binding.chatList.layoutManager = layoutManager
|
||||
|
||||
val swipeConfiguration = RecyclerViewSwipeConfiguration()
|
||||
val white = ContextCompat.getColor(requireContext(), R.color.white_color)
|
||||
|
||||
swipeConfiguration.rightToLeftAction = RecyclerViewSwipeConfiguration.Action("Delete", white, ContextCompat.getColor(requireContext(), R.color.red_color))
|
||||
val swipeListener = object : RecyclerViewSwipeListener {
|
||||
override fun onLeftToRightSwipe(viewHolder: RecyclerView.ViewHolder) {}
|
||||
|
||||
override fun onRightToLeftSwipe(viewHolder: RecyclerView.ViewHolder) {
|
||||
val viewModel = DialogViewModel(getString(R.string.dialog_default_delete_message))
|
||||
val dialog: Dialog = DialogUtils.getDialog(requireContext(), viewModel)
|
||||
|
||||
viewModel.showCancelButton {
|
||||
adapter.notifyItemChanged(viewHolder.adapterPosition)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
viewModel.showDeleteButton({
|
||||
listViewModel.deleteChatRoom(listViewModel.chatRooms.value?.get(viewHolder.adapterPosition))
|
||||
dialog.dismiss()
|
||||
}, getString(R.string.dialog_delete))
|
||||
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
RecyclerViewSwipeUtils(ItemTouchHelper.LEFT, swipeConfiguration, swipeListener)
|
||||
.attachToRecyclerView(binding.chatList)
|
||||
|
||||
// Divider between items
|
||||
val dividerItemDecoration = DividerItemDecoration(context, layoutManager.orientation)
|
||||
dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider, null))
|
||||
binding.chatList.addItemDecoration(dividerItemDecoration)
|
||||
|
||||
listViewModel.chatRooms.observe(viewLifecycleOwner, Observer { chatRooms ->
|
||||
adapter.submitList(chatRooms)
|
||||
})
|
||||
|
||||
listViewModel.latestUpdatedChatRoomId.observe(viewLifecycleOwner, Observer { position ->
|
||||
adapter.notifyItemChanged(position)
|
||||
})
|
||||
|
||||
listViewModel.contactsUpdatedEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume {
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
})
|
||||
|
||||
adapter.selectedChatRoomEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { chatRoom ->
|
||||
sharedViewModel.selectedChatRoom.value = chatRoom
|
||||
if (!resources.getBoolean(R.bool.isTablet)) {
|
||||
if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) {
|
||||
findNavController().navigate(R.id.action_masterChatRoomsFragment_to_detailChatRoomFragment)
|
||||
}
|
||||
} else {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(R.id.action_global_detailChatRoomFragment)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
binding.setEditClickListener {
|
||||
listSelectionViewModel.isEditionEnabled.value = true
|
||||
}
|
||||
|
||||
binding.setNewOneToOneChatRoomClickListener {
|
||||
val bundle = bundleOf("createGroup" to false)
|
||||
if (!resources.getBoolean(R.bool.isTablet)) {
|
||||
if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_masterChatRoomsFragment_to_chatRoomCreationFragment,
|
||||
bundle
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(R.id.action_global_chatRoomCreationFragment, bundle)
|
||||
}
|
||||
}
|
||||
|
||||
binding.setNewGroupChatRoomClickListener {
|
||||
sharedViewModel.selectedGroupChatRoom.value = null
|
||||
|
||||
val bundle = bundleOf("createGroup" to true)
|
||||
if (!resources.getBoolean(R.bool.isTablet)) {
|
||||
if (findNavController().currentDestination?.id == R.id.masterChatRoomsFragment) {
|
||||
findNavController().navigate(
|
||||
R.id.action_masterChatRoomsFragment_to_chatRoomCreationFragment,
|
||||
bundle
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val navHostFragment =
|
||||
childFragmentManager.findFragmentById(R.id.chat_nav_container) as NavHostFragment
|
||||
navHostFragment.navController.navigate(R.id.action_global_chatRoomCreationFragment, bundle)
|
||||
}
|
||||
}
|
||||
|
||||
val localSipUri = arguments?.getString("LocalSipUri")
|
||||
val remoteSipUri = arguments?.getString("RemoteSipUri")
|
||||
if (localSipUri != null && remoteSipUri != null) {
|
||||
Log.i("[Chat] Found local ($localSipUri) & remote addresses ($remoteSipUri) in arguments")
|
||||
arguments?.clear()
|
||||
val localAddress = Factory.instance().createAddress(localSipUri)
|
||||
val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
|
||||
val chatRoom = coreContext.core.getChatRoom(remoteSipAddress, localAddress)
|
||||
if (chatRoom != null) {
|
||||
Log.i("[Chat] Found matching chat room $chatRoom")
|
||||
chatRoom.markAsRead()
|
||||
adapter.selectedChatRoomEvent.value = Event(chatRoom)
|
||||
}
|
||||
} else {
|
||||
sharedViewModel.filesToShare.observe(viewLifecycleOwner, Observer {
|
||||
if (it.isNotEmpty()) {
|
||||
Log.i("[Chat] Found ${it.size} files to share")
|
||||
val activity = requireActivity() as MainActivity
|
||||
activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing)
|
||||
}
|
||||
})
|
||||
sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner, Observer {
|
||||
if (!it.consumed()) {
|
||||
Log.i("[Chat] Found chat message to transfer")
|
||||
|
||||
val activity = requireActivity() as MainActivity
|
||||
activity.showSnackBar(R.string.chat_room_toast_choose_for_sharing)
|
||||
}
|
||||
})
|
||||
|
||||
listViewModel.onErrorEvent.observe(viewLifecycleOwner, Observer {
|
||||
it.consume { messageResourceId ->
|
||||
(activity as MainActivity).showSnackBar(messageResourceId)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return adapter.itemCount
|
||||
}
|
||||
|
||||
override fun deleteItems(indexesOfItemToDelete: ArrayList<Int>) {
|
||||
val list = ArrayList<ChatRoom>()
|
||||
for (index in indexesOfItemToDelete) {
|
||||
val chatRoom = adapter.getItemAt(index)
|
||||
list.add(chatRoom)
|
||||
}
|
||||
listViewModel.deleteChatRooms(list)
|
||||
}
|
||||
|
||||
private fun scrollToTop() {
|
||||
binding.chatList.scrollToPosition(0)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
|
@ -17,19 +17,19 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.chat;
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import org.linphone.R;
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class DeviceChildViewHolder {
|
||||
public final TextView deviceName;
|
||||
public final ImageView securityLevel;
|
||||
class ChatMessageAttachmentViewModel(
|
||||
val path: String,
|
||||
val isImage: Boolean,
|
||||
private val deleteCallback: (attachment: ChatMessageAttachmentViewModel) -> Unit
|
||||
) : ViewModel() {
|
||||
val fileName: String = FileUtils.getNameFromFilePath(path)
|
||||
|
||||
public DeviceChildViewHolder(View v) {
|
||||
deviceName = v.findViewById(R.id.name);
|
||||
securityLevel = v.findViewById(R.id.security_level);
|
||||
fun delete() {
|
||||
deleteCallback(this)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.Content
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class ChatMessageContentViewModel(
|
||||
val content: Content,
|
||||
private val chatMessage: ChatMessage,
|
||||
private val listener: OnContentClickedListener?
|
||||
) : ViewModel() {
|
||||
val isImage = MutableLiveData<Boolean>()
|
||||
|
||||
val downloadable = MutableLiveData<Boolean>()
|
||||
|
||||
val downloadEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isAlone: Boolean
|
||||
get() {
|
||||
var count = 0
|
||||
for (content in chatMessage.contents) {
|
||||
if (content.isFileTransfer || content.isFile) {
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
return count == 1
|
||||
}
|
||||
|
||||
init {
|
||||
if (content.isFile || (content.isFileTransfer && chatMessage.isOutgoing)) {
|
||||
downloadable.value = content.filePath.isEmpty()
|
||||
|
||||
if (content.filePath.isNotEmpty()) {
|
||||
Log.i("[Content] Found displayable content: ${content.filePath}")
|
||||
isImage.value = FileUtils.isExtensionImage(content.filePath)
|
||||
} else {
|
||||
Log.w("[Content] Found content with empty path...")
|
||||
isImage.value = false
|
||||
}
|
||||
} else {
|
||||
Log.i("[Content] Found downloadable content: ${content.name}")
|
||||
downloadable.value = true
|
||||
isImage.value = false
|
||||
}
|
||||
|
||||
downloadEnabled.value = downloadable.value
|
||||
}
|
||||
|
||||
fun download() {
|
||||
if (content.isFileTransfer && (content.filePath == null || content.filePath.isEmpty())) {
|
||||
val file = FileUtils.getFileStoragePath(content.name)
|
||||
content.filePath = file.path
|
||||
downloadEnabled.value = false
|
||||
|
||||
Log.i("[Content] Started downloading ${content.name} into ${content.filePath}")
|
||||
chatMessage.downloadContent(content)
|
||||
}
|
||||
}
|
||||
|
||||
fun openFile() {
|
||||
listener?.onContentClicked(content.filePath)
|
||||
}
|
||||
}
|
||||
|
||||
interface OnContentClickedListener {
|
||||
fun onContentClicked(path: String)
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.io.File
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.core.*
|
||||
import org.linphone.utils.FileUtils
|
||||
|
||||
class ChatMessageSendingViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return ChatMessageSendingViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
var temporaryFileUploadPath: File? = null
|
||||
|
||||
val attachments = MutableLiveData<ArrayList<ChatMessageAttachmentViewModel>>()
|
||||
|
||||
val attachFileEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val sendMessageEnabled = MutableLiveData<Boolean>()
|
||||
|
||||
val isReadOnly = MutableLiveData<Boolean>()
|
||||
|
||||
var textToSend: String = ""
|
||||
set(value) {
|
||||
sendMessageEnabled.value = value.isNotEmpty() || attachments.value?.isNotEmpty() ?: false
|
||||
if (value.isNotEmpty()) {
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
}
|
||||
chatRoom.compose()
|
||||
} else {
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = attachments.value?.isEmpty() ?: true
|
||||
}
|
||||
}
|
||||
field = value
|
||||
}
|
||||
|
||||
init {
|
||||
attachments.value = arrayListOf()
|
||||
|
||||
attachFileEnabled.value = true
|
||||
sendMessageEnabled.value = false
|
||||
isReadOnly.value = chatRoom.hasBeenLeft()
|
||||
}
|
||||
|
||||
fun addAttachment(path: String) {
|
||||
val list = arrayListOf<ChatMessageAttachmentViewModel>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
list.add(ChatMessageAttachmentViewModel(path, FileUtils.isExtensionImage(path)) {
|
||||
removeAttachment(it)
|
||||
})
|
||||
attachments.value = list
|
||||
|
||||
sendMessageEnabled.value = textToSend.isNotEmpty() || list.isNotEmpty()
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeAttachment(attachment: ChatMessageAttachmentViewModel) {
|
||||
val list = arrayListOf<ChatMessageAttachmentViewModel>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
list.remove(attachment)
|
||||
attachments.value = list
|
||||
|
||||
sendMessageEnabled.value = textToSend.isNotEmpty() || list.isNotEmpty()
|
||||
if (!corePreferences.allowMultipleFilesAndTextInSameMessage) {
|
||||
attachFileEnabled.value = list.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun sendMessage() {
|
||||
val isBasicChatRoom: Boolean = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
val message: ChatMessage = chatRoom.createEmptyMessage()
|
||||
|
||||
if (textToSend.isNotEmpty()) {
|
||||
message.addTextContent(textToSend)
|
||||
}
|
||||
|
||||
for (attachment in attachments.value.orEmpty()) {
|
||||
val content = Factory.instance().createContent()
|
||||
|
||||
if (attachment.isImage) {
|
||||
content.type = "image"
|
||||
} else {
|
||||
content.type = "file"
|
||||
}
|
||||
content.subtype = FileUtils.getExtensionFromFileName(attachment.fileName)
|
||||
content.name = attachment.fileName
|
||||
content.filePath = attachment.path // Let the file body handler take care of the upload
|
||||
|
||||
if (isBasicChatRoom) {
|
||||
val fileMessage: ChatMessage = chatRoom.createFileTransferMessage(content)
|
||||
fileMessage.send()
|
||||
} else {
|
||||
message.addFileContent(content)
|
||||
}
|
||||
}
|
||||
|
||||
if (message.contents.isNotEmpty()) {
|
||||
message.send()
|
||||
}
|
||||
|
||||
attachments.value = arrayListOf()
|
||||
}
|
||||
|
||||
fun transferMessage(chatMessage: ChatMessage) {
|
||||
val message = chatRoom.createForwardMessage(chatMessage)
|
||||
message?.send()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import android.os.CountDownTimer
|
||||
import android.text.Spanned
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.LinphoneApplication.Companion.corePreferences
|
||||
import org.linphone.R
|
||||
import org.linphone.compatibility.Compatibility
|
||||
import org.linphone.contact.GenericContactViewModel
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatMessageListenerStub
|
||||
import org.linphone.core.Content
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.mediastream.Version
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatMessageViewModel(
|
||||
val chatMessage: ChatMessage,
|
||||
private val contentListener: OnContentClickedListener? = null
|
||||
) : GenericContactViewModel(chatMessage.fromAddress) {
|
||||
val sendInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val transferInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val showImdn = MutableLiveData<Boolean>()
|
||||
|
||||
val imdnIcon = MutableLiveData<Int>()
|
||||
|
||||
val backgroundRes = MutableLiveData<Int>()
|
||||
|
||||
val hideAvatar = MutableLiveData<Boolean>()
|
||||
|
||||
val hideTime = MutableLiveData<Boolean>()
|
||||
|
||||
val contents = MutableLiveData<ArrayList<ChatMessageContentViewModel>>()
|
||||
|
||||
val time = MutableLiveData<String>()
|
||||
|
||||
val ephemeralLifetime = MutableLiveData<String>()
|
||||
|
||||
val text: Spanned? by lazy {
|
||||
if (chatMessage.textContent != null) AppUtils.getTextWithHttpLinks(chatMessage.textContent) else null
|
||||
}
|
||||
|
||||
private var countDownTimer: CountDownTimer? = null
|
||||
|
||||
private val listener = object : ChatMessageListenerStub() {
|
||||
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
|
||||
time.value = TimestampUtils.toString(chatMessage.time)
|
||||
updateChatMessageState(state)
|
||||
|
||||
// TODO FIXME : find a way to refresh outgoing message downloaded
|
||||
if (state == ChatMessage.State.FileTransferDone && !message.isOutgoing) {
|
||||
Log.i("[Chat Message] File transfer done")
|
||||
// No need to refresh content lists on outgoing messages after file transfer is done
|
||||
// It will even cause the app to crash if updateContentsList is not call right after
|
||||
updateContentsList()
|
||||
|
||||
if (!message.isEphemeral && corePreferences.makePublicDownloadedImages) {
|
||||
if (Version.sdkAboveOrEqual(Version.API29_ANDROID_10) || PermissionHelper.get().hasWriteExternalStorage()) {
|
||||
for (content in message.contents) {
|
||||
if (content.isFile && content.filePath != null && content.userData == null) {
|
||||
addContentToMediaStore(content)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.e("[Chat Message] Can't make file public, app doesn't have WRITE_EXTERNAL_STORAGE permission")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEphemeralMessageTimerStarted(message: ChatMessage) {
|
||||
updateEphemeralTimer()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatMessage.addListener(listener)
|
||||
|
||||
backgroundRes.value = if (chatMessage.isOutgoing) R.drawable.chat_bubble_outgoing_full else R.drawable.chat_bubble_incoming_full
|
||||
hideAvatar.value = false
|
||||
time.value = TimestampUtils.toString(chatMessage.time)
|
||||
updateEphemeralTimer()
|
||||
|
||||
updateChatMessageState(chatMessage.state)
|
||||
updateContentsList()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
chatMessage.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun updateBubbleBackground(hasPrevious: Boolean, hasNext: Boolean) {
|
||||
if (hasPrevious) {
|
||||
hideTime.value = true
|
||||
}
|
||||
|
||||
if (chatMessage.isOutgoing) {
|
||||
if (hasNext && hasPrevious) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_2
|
||||
} else if (hasNext) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_1
|
||||
} else if (hasPrevious) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_split_3
|
||||
} else {
|
||||
backgroundRes.value = R.drawable.chat_bubble_outgoing_full
|
||||
}
|
||||
} else {
|
||||
if (hasNext && hasPrevious) {
|
||||
hideAvatar.value = true
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_2
|
||||
} else if (hasNext) {
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_1
|
||||
} else if (hasPrevious) {
|
||||
hideAvatar.value = true
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_split_3
|
||||
} else {
|
||||
backgroundRes.value = R.drawable.chat_bubble_incoming_full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChatMessageState(state: ChatMessage.State) {
|
||||
transferInProgress.value = state == ChatMessage.State.FileTransferInProgress
|
||||
|
||||
sendInProgress.value = state == ChatMessage.State.InProgress || state == ChatMessage.State.FileTransferInProgress
|
||||
|
||||
showImdn.value = when (state) {
|
||||
ChatMessage.State.DeliveredToUser, ChatMessage.State.Displayed, ChatMessage.State.NotDelivered -> true
|
||||
else -> false
|
||||
}
|
||||
|
||||
imdnIcon.value = when (state) {
|
||||
ChatMessage.State.DeliveredToUser -> R.drawable.imdn_received
|
||||
ChatMessage.State.Displayed -> R.drawable.imdn_read
|
||||
else -> R.drawable.imdn_error
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateContentsList() {
|
||||
val list = arrayListOf<ChatMessageContentViewModel>()
|
||||
for (content in chatMessage.contents) {
|
||||
if (content.isFileTransfer || content.isFile) {
|
||||
list.add(ChatMessageContentViewModel(content, chatMessage, contentListener))
|
||||
}
|
||||
}
|
||||
contents.value = list
|
||||
}
|
||||
|
||||
private fun updateEphemeralTimer() {
|
||||
if (chatMessage.isEphemeral) {
|
||||
if (chatMessage.ephemeralExpireTime == 0L) {
|
||||
// This means the message hasn't been read by all participants yet, so the countdown hasn't started
|
||||
// In this case we simply display the configured value for lifetime
|
||||
ephemeralLifetime.value = formatLifetime(chatMessage.ephemeralLifetime)
|
||||
} else {
|
||||
// Countdown has started, display remaining time
|
||||
val remaining = chatMessage.ephemeralExpireTime - (System.currentTimeMillis() / 1000)
|
||||
ephemeralLifetime.value = formatLifetime(remaining)
|
||||
if (countDownTimer == null) {
|
||||
countDownTimer = object : CountDownTimer(remaining * 1000, 1000) {
|
||||
override fun onFinish() {}
|
||||
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
ephemeralLifetime.postValue(formatLifetime(millisUntilFinished / 1000))
|
||||
}
|
||||
}
|
||||
countDownTimer?.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLifetime(seconds: Long): String {
|
||||
val days = seconds / 86400
|
||||
return when {
|
||||
days >= 1L -> AppUtils.getStringWithPlural(R.plurals.days, days.toInt())
|
||||
else -> String.format("%02d:%02d:%02d", seconds / 3600, (seconds % 3600) / 60, (seconds % 60))
|
||||
}
|
||||
}
|
||||
|
||||
private fun addContentToMediaStore(content: Content) {
|
||||
when (content.type) {
|
||||
"image" -> {
|
||||
if (Compatibility.addImageToMediaStore(coreContext.context, content)) {
|
||||
Log.i("[Chat Message] Adding image ${content.name} terminated")
|
||||
} else {
|
||||
Log.e("[Chat Message] Something went wrong while copying file...")
|
||||
}
|
||||
}
|
||||
"video" -> {
|
||||
if (Compatibility.addVideoToMediaStore(coreContext.context, content)) {
|
||||
Log.i("[Chat Message] Adding video ${content.name} terminated")
|
||||
} else {
|
||||
Log.e("[Chat Message] Something went wrong while copying file...")
|
||||
}
|
||||
}
|
||||
"audio" -> {
|
||||
if (Compatibility.addAudioToMediaStore(coreContext.context, content)) {
|
||||
Log.i("[Chat Message] Adding audio ${content.name} terminated")
|
||||
} else {
|
||||
Log.e("[Chat Message] Something went wrong while copying file...")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.w("[Chat Message] File ${content.name} isn't either an image, an audio file or a video, can't add it to the Media Store")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import java.util.*
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.mediastream.Version
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.PermissionHelper
|
||||
|
||||
class ChatMessagesListViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return ChatMessagesListViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatMessagesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
companion object {
|
||||
private const val MESSAGES_PER_PAGE = 20
|
||||
}
|
||||
|
||||
val events = MutableLiveData<ArrayList<EventLog>>()
|
||||
|
||||
val messageUpdatedEvent: MutableLiveData<Event<Int>> by lazy {
|
||||
MutableLiveData<Event<Int>>()
|
||||
}
|
||||
|
||||
val requestWriteExternalStoragePermissionEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
private val chatMessageListener: ChatMessageListenerStub = object : ChatMessageListenerStub() {
|
||||
override fun onMsgStateChanged(message: ChatMessage, state: ChatMessage.State) {
|
||||
if (state == ChatMessage.State.Displayed) {
|
||||
message.removeListener(this)
|
||||
}
|
||||
|
||||
val position: Int? = message.userData as? Int?
|
||||
if (position != null) {
|
||||
messageUpdatedEvent.value = Event(position)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
chatRoom.markAsRead()
|
||||
|
||||
val position = events.value?.size ?: 0
|
||||
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
chatMessage.userData = position
|
||||
chatMessage.addListener(chatMessageListener)
|
||||
|
||||
if (Version.sdkStrictlyBelow(Version.API29_ANDROID_10) && !PermissionHelper.get().hasWriteExternalStorage()) {
|
||||
for (content in chatMessage.contents) {
|
||||
if (content.isFileTransfer) {
|
||||
Log.i("[Chat Messages] Android < 10 detected and WRITE_EXTERNAL_STORAGE permission isn't granted yet")
|
||||
requestWriteExternalStoragePermissionEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
val position = events.value?.size ?: 0
|
||||
|
||||
if (eventLog.type == EventLog.Type.ConferenceChatMessage) {
|
||||
val chatMessage = eventLog.chatMessage
|
||||
chatMessage.userData = position
|
||||
chatMessage.addListener(chatMessageListener)
|
||||
}
|
||||
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onConferenceLeft(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
|
||||
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
Log.i("[Chat Messages] An ephemeral chat message has expired, removing it from event list")
|
||||
deleteMessage(eventLog.chatMessage)
|
||||
}
|
||||
|
||||
override fun onEphemeralEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
addEvent(eventLog)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
|
||||
events.value = getEvents()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun resendMessage(chatMessage: ChatMessage) {
|
||||
val position: Int = chatMessage.userData as Int
|
||||
chatMessage.resend()
|
||||
messageUpdatedEvent.value = Event(position)
|
||||
}
|
||||
|
||||
fun deleteMessage(chatMessage: ChatMessage) {
|
||||
val position: Int = chatMessage.userData as Int
|
||||
LinphoneUtils.deleteFilesAttachedToChatMessage(chatMessage)
|
||||
chatRoom.deleteMessage(chatMessage)
|
||||
|
||||
val list = arrayListOf<EventLog>()
|
||||
list.addAll(events.value.orEmpty())
|
||||
list.removeAt(position)
|
||||
events.value = list
|
||||
}
|
||||
|
||||
fun deleteEventLogs(listToDelete: ArrayList<EventLog>) {
|
||||
val list = arrayListOf<EventLog>()
|
||||
list.addAll(events.value.orEmpty())
|
||||
|
||||
for (eventLog in listToDelete) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||
eventLog.deleteFromDatabase()
|
||||
list.remove(eventLog)
|
||||
}
|
||||
|
||||
events.value = list
|
||||
}
|
||||
|
||||
fun loadMoreData(totalItemsCount: Int) {
|
||||
Log.i("[Chat Messages] Load more data, current total is $totalItemsCount")
|
||||
val maxSize: Int = chatRoom.historyEventsSize
|
||||
|
||||
if (totalItemsCount < maxSize) {
|
||||
var upperBound: Int = totalItemsCount + MESSAGES_PER_PAGE
|
||||
if (upperBound > maxSize) {
|
||||
upperBound = maxSize
|
||||
}
|
||||
|
||||
val history: Array<EventLog> = chatRoom.getHistoryRangeEvents(totalItemsCount, upperBound)
|
||||
val list = arrayListOf<EventLog>()
|
||||
for (message in history) {
|
||||
list.add(message)
|
||||
}
|
||||
list.addAll(events.value.orEmpty())
|
||||
events.value = list
|
||||
}
|
||||
}
|
||||
|
||||
private fun addEvent(eventLog: EventLog) {
|
||||
val list = arrayListOf<EventLog>()
|
||||
list.addAll(events.value.orEmpty())
|
||||
list.add(eventLog)
|
||||
events.value = list
|
||||
}
|
||||
|
||||
private fun getEvents(): ArrayList<EventLog> {
|
||||
val list = arrayListOf<EventLog>()
|
||||
val history = chatRoom.getHistoryEvents(MESSAGES_PER_PAGE)
|
||||
for (message in history) {
|
||||
list.add(message)
|
||||
}
|
||||
return list
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.contact.Contact
|
||||
import org.linphone.contact.ContactViewModelInterface
|
||||
import org.linphone.core.*
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomCreationContactViewModel(private val searchResult: SearchResult) : ViewModel(), ContactViewModelInterface {
|
||||
override val contact = MutableLiveData<Contact>()
|
||||
|
||||
override val displayName: String by lazy {
|
||||
when {
|
||||
searchResult.friend != null -> searchResult.friend.name
|
||||
searchResult.address != null -> LinphoneUtils.getDisplayName(searchResult.address)
|
||||
else -> searchResult.phoneNumber
|
||||
}
|
||||
}
|
||||
|
||||
val isDisabled: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
|
||||
val isSelected: MutableLiveData<Boolean> by lazy {
|
||||
MutableLiveData<Boolean>()
|
||||
}
|
||||
|
||||
val isLinphoneUser: Boolean by lazy {
|
||||
searchResult.friend?.getPresenceModelForUriOrTel(searchResult.phoneNumber ?: searchResult.address.asStringUriOnly())?.basicStatus == PresenceBasicStatus.Open
|
||||
}
|
||||
|
||||
val sipUri: String by lazy {
|
||||
searchResult.phoneNumber ?: searchResult.address.asStringUriOnly()
|
||||
}
|
||||
|
||||
val address: Address by lazy {
|
||||
searchResult.address
|
||||
}
|
||||
|
||||
val hasLimeX3DHCapability: Boolean
|
||||
get() = searchResult.hasCapability(FriendCapability.LimeX3Dh)
|
||||
|
||||
var listener: ChatRoomCreationContactSelectionListener? = null
|
||||
|
||||
init {
|
||||
isDisabled.value = false
|
||||
isSelected.value = false
|
||||
searchMatchingContact()
|
||||
}
|
||||
|
||||
private fun searchMatchingContact() {
|
||||
if (searchResult.address != null) {
|
||||
contact.value =
|
||||
coreContext.contactsManager.findContactByAddress(searchResult.address)
|
||||
} else if (searchResult.phoneNumber != null) {
|
||||
contact.value = coreContext.contactsManager.findContactByPhoneNumber(searchResult.phoneNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface ChatRoomCreationContactSelectionListener {
|
||||
fun onUnSelected(searchResult: SearchResult)
|
||||
}
|
|
@ -0,0 +1,202 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.ErrorReportingViewModel
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomCreationViewModel : ErrorReportingViewModel() {
|
||||
val chatRoomCreatedEvent: MutableLiveData<Event<ChatRoom>> by lazy {
|
||||
MutableLiveData<Event<ChatRoom>>()
|
||||
}
|
||||
|
||||
val createGroupChat = MutableLiveData<Boolean>()
|
||||
|
||||
val sipContactsSelected = MutableLiveData<Boolean>()
|
||||
|
||||
val isEncrypted = MutableLiveData<Boolean>()
|
||||
|
||||
val contactsList = MutableLiveData<ArrayList<SearchResult>>()
|
||||
|
||||
val waitForChatRoomCreation = MutableLiveData<Boolean>()
|
||||
|
||||
val selectedAddresses = MutableLiveData<ArrayList<Address>>()
|
||||
|
||||
val limeAvailable: Boolean = LinphoneUtils.isLimeAvailable()
|
||||
|
||||
private var filter: String = ""
|
||||
|
||||
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
Log.i("[Chat Room Creation] Contacts have changed")
|
||||
updateContactsList()
|
||||
}
|
||||
}
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(room: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
waitForChatRoomCreation.value = false
|
||||
Log.i("[Chat Room Creation] Chat room created")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
} else if (state == ChatRoom.State.CreationFailed) {
|
||||
Log.e("[Chat Room Creation] Group chat room creation has failed !")
|
||||
waitForChatRoomCreation.value = false
|
||||
onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
createGroupChat.value = false
|
||||
sipContactsSelected.value = true
|
||||
isEncrypted.value = false
|
||||
|
||||
selectedAddresses.value = arrayListOf()
|
||||
|
||||
updateContactsList()
|
||||
|
||||
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun updateEncryption(encrypted: Boolean) {
|
||||
isEncrypted.value = encrypted
|
||||
}
|
||||
|
||||
fun filter(search: String) {
|
||||
if (filter.isNotEmpty() && filter.length > search.length) {
|
||||
coreContext.contactsManager.magicSearch.resetSearchCache()
|
||||
}
|
||||
filter = search
|
||||
|
||||
updateContactsList()
|
||||
}
|
||||
|
||||
fun updateContactsList() {
|
||||
val domain = if (sipContactsSelected.value == true) coreContext.core.defaultProxyConfig?.domain ?: "" else ""
|
||||
val results = coreContext.contactsManager.magicSearch.getContactListFromFilter(filter, domain)
|
||||
|
||||
val list = arrayListOf<SearchResult>()
|
||||
for (result in results) {
|
||||
list.add(result)
|
||||
}
|
||||
contactsList.value = list
|
||||
}
|
||||
|
||||
fun toggleSelectionForSearchResult(searchResult: SearchResult) {
|
||||
if (searchResult.address != null) {
|
||||
toggleSelectionForAddress(searchResult.address)
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSelectionForAddress(address: Address) {
|
||||
val list = arrayListOf<Address>()
|
||||
list.addAll(selectedAddresses.value.orEmpty())
|
||||
|
||||
val found = list.find {
|
||||
if (address != null) it.weakEqual(address) else false
|
||||
}
|
||||
|
||||
if (found != null) {
|
||||
list.remove(found)
|
||||
} else {
|
||||
val contact = coreContext.contactsManager.findContactByAddress(address)
|
||||
if (contact != null) address.displayName = contact.fullName
|
||||
list.add(address)
|
||||
}
|
||||
|
||||
selectedAddresses.value = list
|
||||
}
|
||||
|
||||
fun createOneToOneChat(searchResult: SearchResult) {
|
||||
waitForChatRoomCreation.value = true
|
||||
val defaultProxyConfig = coreContext.core.defaultProxyConfig
|
||||
var room: ChatRoom?
|
||||
|
||||
if (defaultProxyConfig == null) {
|
||||
val address = searchResult.address ?: coreContext.core.interpretUrl(searchResult.phoneNumber)
|
||||
if (address == null) {
|
||||
Log.e("[Chat Room Creation] Can't get a valid address from search result $searchResult")
|
||||
onErrorEvent.value = Event(R.string.chat_room_creation_failed_snack)
|
||||
waitForChatRoomCreation.value = false
|
||||
return
|
||||
}
|
||||
|
||||
Log.w("[Chat Room Creation] No default proxy config found, creating basic chat room without local identity with ${address.asStringUriOnly()}")
|
||||
room = coreContext.core.getChatRoom(address)
|
||||
if (room != null) {
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
} else {
|
||||
Log.e("[Chat Room Creation] Couldn't create chat room with remote ${address.asStringUriOnly()}")
|
||||
}
|
||||
waitForChatRoomCreation.value = false
|
||||
return
|
||||
}
|
||||
|
||||
val encrypted = isEncrypted.value == true
|
||||
room = coreContext.core.findOneToOneChatRoom(defaultProxyConfig.identityAddress, searchResult.address, encrypted)
|
||||
if (room == null) {
|
||||
Log.w("[Chat Room Creation] Couldn't find existing 1-1 chat room with remote ${searchResult.address.asStringUriOnly()}, encryption=$encrypted and local identity ${defaultProxyConfig.identityAddress.asStringUriOnly()}")
|
||||
if (encrypted) {
|
||||
val params: ChatRoomParams = coreContext.core.createDefaultChatRoomParams()
|
||||
// This will set the backend to FlexisipChat automatically
|
||||
params.enableEncryption(true)
|
||||
params.enableGroup(false)
|
||||
|
||||
val participants = arrayOfNulls<Address>(1)
|
||||
participants[0] = searchResult.address
|
||||
|
||||
room = coreContext.core.createChatRoom(
|
||||
params,
|
||||
AppUtils.getString(R.string.chat_room_dummy_subject),
|
||||
participants
|
||||
)
|
||||
room?.addListener(listener)
|
||||
} else {
|
||||
room = coreContext.core.getChatRoom(searchResult.address, defaultProxyConfig.identityAddress)
|
||||
if (room != null) {
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
} else {
|
||||
Log.e("[Chat Room Creation] Couldn't create chat room with remote ${searchResult.address.asStringUriOnly()} and local identity ${defaultProxyConfig.identityAddress.asStringUriOnly()}")
|
||||
}
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
} else {
|
||||
Log.i("[Chat Room Creation] Found existing 1-1 chat room with remote ${searchResult.address.asStringUriOnly()}, encryption=$encrypted and local identity ${defaultProxyConfig.identityAddress.asStringUriOnly()}")
|
||||
chatRoomCreatedEvent.value = Event(room)
|
||||
waitForChatRoomCreation.value = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,292 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.Contact
|
||||
import org.linphone.contact.ContactViewModelInterface
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
import org.linphone.utils.TimestampUtils
|
||||
|
||||
class ChatRoomViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return ChatRoomViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class ChatRoomViewModel(val chatRoom: ChatRoom) : ViewModel(), ContactViewModelInterface {
|
||||
override val contact = MutableLiveData<Contact>()
|
||||
|
||||
override val displayName: String by lazy {
|
||||
when {
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()) -> LinphoneUtils.getDisplayName(chatRoom.peerAddress)
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) -> LinphoneUtils.getDisplayName(chatRoom.participants.first()?.address ?: chatRoom.peerAddress)
|
||||
chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) -> chatRoom.subject
|
||||
else -> chatRoom.peerAddress.asStringUriOnly()
|
||||
}
|
||||
}
|
||||
|
||||
override val securityLevel: ChatRoomSecurityLevel
|
||||
get() = chatRoom.securityLevel
|
||||
|
||||
override val showGroupChatAvatar: Boolean
|
||||
get() = chatRoom.hasCapability(ChatRoomCapabilities.Conference.toInt()) && !chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
||||
|
||||
val subject = MutableLiveData<String>()
|
||||
|
||||
val participants = MutableLiveData<String>()
|
||||
|
||||
val unreadMessagesCount = MutableLiveData<Int>()
|
||||
|
||||
val lastUpdate = MutableLiveData<String>()
|
||||
|
||||
val lastMessageText = MutableLiveData<String>()
|
||||
|
||||
val callInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val remoteIsComposing = MutableLiveData<Boolean>()
|
||||
|
||||
val composingList = MutableLiveData<String>()
|
||||
|
||||
val securityLevelIcon = MutableLiveData<Int>()
|
||||
|
||||
val securityLevelContentDescription = MutableLiveData<Int>()
|
||||
|
||||
val oneToOneChatRoom: Boolean
|
||||
get() = chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())
|
||||
|
||||
val encryptedChatRoom: Boolean
|
||||
get() = chatRoom.hasCapability(ChatRoomCapabilities.Encrypted.toInt())
|
||||
|
||||
val basicChatRoom: Boolean
|
||||
get() = chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())
|
||||
|
||||
val peerSipUri: String
|
||||
get() = chatRoom.peerAddress.asStringUriOnly()
|
||||
|
||||
val oneParticipantOneDevice: Boolean
|
||||
get() {
|
||||
return chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt()) &&
|
||||
chatRoom.me.devices.size == 1 &&
|
||||
chatRoom.participants.first().devices.size == 1
|
||||
}
|
||||
|
||||
val addressToCall: Address
|
||||
get() {
|
||||
return if (chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt()))
|
||||
chatRoom.peerAddress
|
||||
else
|
||||
chatRoom.participants.first().address
|
||||
}
|
||||
|
||||
val onlyParticipantOnlyDeviceAddress: Address
|
||||
get() = chatRoom.participants.first().devices.first().address
|
||||
|
||||
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
Log.i("[Chat Room] Contacts have changed")
|
||||
contactLookup()
|
||||
}
|
||||
}
|
||||
|
||||
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onCallStateChanged(
|
||||
core: Core,
|
||||
call: Call,
|
||||
state: Call.State,
|
||||
message: String
|
||||
) {
|
||||
callInProgress.value = core.callsNb > 0
|
||||
}
|
||||
}
|
||||
|
||||
private val chatRoomListener: ChatRoomListenerStub = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
Log.i("[Chat Room] $chatRoom state changed: $state")
|
||||
}
|
||||
|
||||
override fun onSubjectChanged(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
subject.value = chatRoom.subject
|
||||
}
|
||||
|
||||
override fun onChatMessageReceived(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
unreadMessagesCount.value = chatRoom.unreadMessagesCount
|
||||
lastMessageText.value = formatLastMessage(eventLog.chatMessage)
|
||||
}
|
||||
|
||||
override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
lastMessageText.value = formatLastMessage(eventLog.chatMessage)
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
}
|
||||
|
||||
override fun onIsComposingReceived(
|
||||
chatRoom: ChatRoom,
|
||||
remoteAddr: Address,
|
||||
isComposing: Boolean
|
||||
) {
|
||||
updateRemotesComposing()
|
||||
}
|
||||
|
||||
override fun onConferenceJoined(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
contactLookup()
|
||||
updateSecurityIcon()
|
||||
}
|
||||
|
||||
override fun onSecurityEvent(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
updateSecurityIcon()
|
||||
}
|
||||
|
||||
override fun onEphemeralMessageDeleted(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
Log.i("[Chat Room] Ephemeral message deleted, updated last message displayed")
|
||||
lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.core.addListener(coreListener)
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
||||
|
||||
lastMessageText.value = formatLastMessage(chatRoom.lastMessageInHistory)
|
||||
unreadMessagesCount.value = chatRoom.unreadMessagesCount
|
||||
lastUpdate.value = TimestampUtils.toString(chatRoom.lastUpdateTime, true)
|
||||
|
||||
subject.value = chatRoom.subject
|
||||
updateSecurityIcon()
|
||||
|
||||
contactLookup()
|
||||
|
||||
callInProgress.value = chatRoom.core.callsNb > 0
|
||||
updateRemotesComposing()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
chatRoom.core.removeListener(coreListener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun contactLookup() {
|
||||
if (chatRoom.hasCapability(ChatRoomCapabilities.OneToOne.toInt())) {
|
||||
searchMatchingContact()
|
||||
} else {
|
||||
getParticipantsNames()
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatLastMessage(msg: ChatMessage?): String {
|
||||
if (msg == null) return ""
|
||||
|
||||
val sender: String =
|
||||
coreContext.contactsManager.findContactByAddress(msg.fromAddress)?.fullName
|
||||
?: LinphoneUtils.getDisplayName(msg.fromAddress)
|
||||
var body = ""
|
||||
for (content in msg.contents) {
|
||||
if (content.isFile || content.isFileTransfer) body += content.name + " "
|
||||
else if (content.isText) body += content.stringBuffer + " "
|
||||
}
|
||||
|
||||
return "$sender: $body"
|
||||
}
|
||||
|
||||
private fun searchMatchingContact() {
|
||||
val remoteAddress = if (chatRoom.hasCapability(ChatRoomCapabilities.Basic.toInt())) {
|
||||
chatRoom.peerAddress
|
||||
} else {
|
||||
if (chatRoom.participants.isNotEmpty()) {
|
||||
chatRoom.participants[0].address
|
||||
} else {
|
||||
Log.e("[Chat Room] $chatRoom doesn't have any participant in state ${chatRoom.state}!")
|
||||
return
|
||||
}
|
||||
}
|
||||
contact.value = coreContext.contactsManager.findContactByAddress(remoteAddress)
|
||||
}
|
||||
|
||||
private fun getParticipantsNames() {
|
||||
if (oneToOneChatRoom) return
|
||||
|
||||
var participantsList = ""
|
||||
var index = 0
|
||||
for (participant in chatRoom.participants) {
|
||||
val contact: Contact? =
|
||||
coreContext.contactsManager.findContactByAddress(participant.address)
|
||||
participantsList += contact?.fullName ?: LinphoneUtils.getDisplayName(participant.address)
|
||||
index++
|
||||
if (index != chatRoom.nbParticipants) participantsList += ", "
|
||||
}
|
||||
participants.value = participantsList
|
||||
}
|
||||
|
||||
private fun updateSecurityIcon() {
|
||||
securityLevelIcon.value = when (chatRoom.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
securityLevelContentDescription.value = when (chatRoom.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemotesComposing() {
|
||||
remoteIsComposing.value = chatRoom.isRemoteComposing
|
||||
|
||||
var composing = ""
|
||||
for (address in chatRoom.composingAddresses) {
|
||||
val contact: Contact? = coreContext.contactsManager.findContactByAddress(address)
|
||||
composing += if (composing.isNotEmpty()) ", " else ""
|
||||
composing += contact?.fullName ?: LinphoneUtils.getDisplayName(address)
|
||||
}
|
||||
composingList.value = AppUtils.getStringWithPlural(R.plurals.chat_room_remote_composing, chatRoom.composingAddresses.size, composing)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.activities.main.viewmodels.ErrorReportingViewModel
|
||||
import org.linphone.contact.ContactsUpdatedListenerStub
|
||||
import org.linphone.core.*
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ChatRoomsListViewModel : ErrorReportingViewModel() {
|
||||
val chatRooms = MutableLiveData<ArrayList<ChatRoom>>()
|
||||
|
||||
val latestUpdatedChatRoomId = MutableLiveData<Int>()
|
||||
|
||||
val contactsUpdatedEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val groupChatAvailable: Boolean = LinphoneUtils.isGroupChatAvailable()
|
||||
|
||||
private val contactsUpdatedListener = object : ContactsUpdatedListenerStub() {
|
||||
override fun onContactsUpdated() {
|
||||
Log.i("[Chat Rooms] Contacts have changed")
|
||||
contactsUpdatedEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
||||
private val listener: CoreListenerStub = object : CoreListenerStub() {
|
||||
override fun onChatRoomStateChanged(core: Core, chatRoom: ChatRoom, state: ChatRoom.State) {
|
||||
if (state == ChatRoom.State.Created) {
|
||||
updateChatRooms()
|
||||
} else if (state == ChatRoom.State.TerminationFailed) {
|
||||
Log.e("[Chat Rooms] Group chat room removal for address ${chatRoom.peerAddress.asStringUriOnly()} has failed !")
|
||||
onErrorEvent.value = Event(R.string.chat_room_removal_failed_snack)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChatRoomSubjectChanged(core: Core, chatRoom: ChatRoom) {
|
||||
updateChatRoom(chatRoom)
|
||||
}
|
||||
|
||||
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
|
||||
updateChatRoom(chatRoom)
|
||||
}
|
||||
|
||||
override fun onMessageSent(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||
if (chatRooms.value?.indexOf(chatRoom) == 0) updateChatRoom(chatRoom)
|
||||
else updateChatRooms()
|
||||
}
|
||||
|
||||
override fun onMessageReceived(core: Core, chatRoom: ChatRoom, message: ChatMessage) {
|
||||
if (chatRooms.value?.indexOf(chatRoom) == 0) updateChatRoom(chatRoom)
|
||||
else updateChatRooms()
|
||||
}
|
||||
|
||||
override fun onMessageReceivedUnableDecrypt(
|
||||
core: Core,
|
||||
chatRoom: ChatRoom,
|
||||
message: ChatMessage
|
||||
) {
|
||||
updateChatRooms()
|
||||
}
|
||||
}
|
||||
|
||||
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||
override fun onStateChanged(chatRoom: ChatRoom, newState: ChatRoom.State) {
|
||||
if (newState == ChatRoom.State.Deleted) {
|
||||
val list = arrayListOf<ChatRoom>()
|
||||
list.addAll(chatRooms.value.orEmpty())
|
||||
list.remove(chatRoom)
|
||||
chatRooms.value = list
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var chatRoomsToDeleteCount = 0
|
||||
|
||||
init {
|
||||
chatRooms.value = getChatRooms()
|
||||
coreContext.core.addListener(listener)
|
||||
coreContext.contactsManager.addListener(contactsUpdatedListener)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
coreContext.contactsManager.removeListener(contactsUpdatedListener)
|
||||
coreContext.core.removeListener(listener)
|
||||
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
fun deleteChatRoom(chatRoom: ChatRoom?) {
|
||||
for (eventLog in chatRoom?.getHistoryMessageEvents(0).orEmpty()) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||
}
|
||||
|
||||
chatRoomsToDeleteCount = 1
|
||||
chatRoom?.addListener(chatRoomListener)
|
||||
chatRoom?.core?.deleteChatRoom(chatRoom)
|
||||
}
|
||||
|
||||
fun deleteChatRooms(chatRooms: ArrayList<ChatRoom>) {
|
||||
chatRoomsToDeleteCount = chatRooms.size
|
||||
for (chatRoom in chatRooms) {
|
||||
for (eventLog in chatRoom.getHistoryMessageEvents(0).orEmpty()) {
|
||||
LinphoneUtils.deleteFilesAttachedToEventLog(eventLog)
|
||||
}
|
||||
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
chatRoom.core?.deleteChatRoom(chatRoom)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChatRoom(chatRoom: ChatRoom) {
|
||||
latestUpdatedChatRoomId.value = chatRooms.value?.indexOf(chatRoom)
|
||||
}
|
||||
|
||||
private fun updateChatRooms() {
|
||||
chatRooms.value = getChatRooms()
|
||||
}
|
||||
|
||||
private fun getChatRooms(): ArrayList<ChatRoom> {
|
||||
val list = arrayListOf<ChatRoom>()
|
||||
|
||||
for (room in coreContext.core.chatRooms) {
|
||||
list.add(room)
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
import org.linphone.core.ParticipantDevice
|
||||
|
||||
class DevicesListChildViewModel(private val device: ParticipantDevice) : ViewModel() {
|
||||
val deviceName: String = device.name
|
||||
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (device.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityContentDescription: Int by lazy {
|
||||
when (device.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
fun onClick() {
|
||||
coreContext.startCall(device.address, true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.GenericContactViewModel
|
||||
import org.linphone.core.ChatRoomSecurityLevel
|
||||
import org.linphone.core.Participant
|
||||
|
||||
class DevicesListGroupViewModel(private val participant: Participant) : GenericContactViewModel(participant.address) {
|
||||
override val securityLevel: ChatRoomSecurityLevel
|
||||
get() = participant.securityLevel
|
||||
|
||||
private val device = if (participant.devices.isEmpty()) null else participant.devices.first()
|
||||
|
||||
val securityLevelIcon: Int by lazy {
|
||||
when (device?.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.drawable.security_2_indicator
|
||||
ChatRoomSecurityLevel.Encrypted -> R.drawable.security_1_indicator
|
||||
else -> R.drawable.security_alert_indicator
|
||||
}
|
||||
}
|
||||
|
||||
val securityLevelContentDescription: Int by lazy {
|
||||
when (device?.securityLevel) {
|
||||
ChatRoomSecurityLevel.Safe -> R.string.content_description_security_level_safe
|
||||
ChatRoomSecurityLevel.Encrypted -> R.string.content_description_security_level_encrypted
|
||||
else -> R.string.content_description_security_level_unsafe
|
||||
}
|
||||
}
|
||||
|
||||
val sipUri: String = participant.address.asStringUriOnly()
|
||||
|
||||
val isExpanded = MutableLiveData<Boolean>()
|
||||
|
||||
val devices = MutableLiveData<ArrayList<DevicesListChildViewModel>>()
|
||||
|
||||
init {
|
||||
isExpanded.value = false
|
||||
|
||||
val list = arrayListOf<DevicesListChildViewModel>()
|
||||
for (device in participant.devices) {
|
||||
list.add(DevicesListChildViewModel((device)))
|
||||
}
|
||||
devices.value = list
|
||||
}
|
||||
|
||||
fun toggleExpanded() {
|
||||
isExpanded.value = isExpanded.value != true
|
||||
}
|
||||
|
||||
fun onClick() {
|
||||
if (device?.address != null) coreContext.startCall(device.address, true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomListenerStub
|
||||
import org.linphone.core.EventLog
|
||||
|
||||
class DevicesListViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return DevicesListViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class DevicesListViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
val participants = MutableLiveData<ArrayList<DevicesListGroupViewModel>>()
|
||||
|
||||
private val listener = object : ChatRoomListenerStub() {
|
||||
override fun onParticipantDeviceAdded(chatRoom: ChatRoom?, eventLog: EventLog?) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantDeviceRemoved(chatRoom: ChatRoom?, eventLog: EventLog?) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom?, eventLog: EventLog?) {
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom?, eventLog: EventLog?) {
|
||||
updateParticipants()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
chatRoom.addListener(listener)
|
||||
updateParticipants()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
chatRoom.removeListener(listener)
|
||||
super.onCleared()
|
||||
}
|
||||
|
||||
private fun updateParticipants() {
|
||||
val list = arrayListOf<DevicesListGroupViewModel>()
|
||||
list.add(DevicesListGroupViewModel(chatRoom.me))
|
||||
for (participant in chatRoom.participants) {
|
||||
list.add(DevicesListGroupViewModel(participant))
|
||||
}
|
||||
participants.value = list
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2019 Belledonne Communications SARL.
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
|
@ -17,21 +17,23 @@
|
|||
* You should have received a copy of the GNU General Public License
|
||||
* 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 android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import org.linphone.R;
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
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;
|
||||
public final TextView participantName, sipUri;
|
||||
|
||||
public CallStatsViewHolder(View v) {
|
||||
avatarLayout = v.findViewById(R.id.avatar_layout);
|
||||
participantName = v.findViewById(R.id.name);
|
||||
sipUri = v.findViewById(R.id.sipUri);
|
||||
fun setSelected() {
|
||||
listener.onDurationValueChanged(duration)
|
||||
}
|
||||
}
|
||||
|
||||
interface DurationItemClicked {
|
||||
fun onDurationValueChanged(duration: Long)
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.linphone.R
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.tools.Log
|
||||
|
||||
class EphemeralViewModelFactory(private val chatRoom: ChatRoom) :
|
||||
ViewModelProvider.NewInstanceFactory() {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return EphemeralViewModel(chatRoom) as T
|
||||
}
|
||||
}
|
||||
|
||||
class EphemeralViewModel(private val chatRoom: ChatRoom) : ViewModel() {
|
||||
val durationsList = MutableLiveData<ArrayList<EphemeralDurationViewModel>>()
|
||||
|
||||
var currentSelectedDuration: Long = 0
|
||||
|
||||
private val listener = object : DurationItemClicked {
|
||||
override fun onDurationValueChanged(duration: Long) {
|
||||
currentSelectedDuration = duration
|
||||
computeEphemeralDurationValues()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
Log.i("[Ephemeral Messages] Current duration is ${chatRoom.ephemeralLifetime}, ephemeral enabled? ${chatRoom.ephemeralEnabled()}")
|
||||
currentSelectedDuration = if (chatRoom.ephemeralEnabled()) chatRoom.ephemeralLifetime else 0
|
||||
computeEphemeralDurationValues()
|
||||
}
|
||||
|
||||
fun updateChatRoomEphemeralDuration() {
|
||||
Log.i("[Ephemeral Messages] Selected value is $currentSelectedDuration")
|
||||
if (currentSelectedDuration > 0) {
|
||||
if (chatRoom.ephemeralLifetime != currentSelectedDuration) {
|
||||
Log.i("[Ephemeral Messages] Setting new lifetime for ephemeral messages to $currentSelectedDuration")
|
||||
chatRoom.ephemeralLifetime = currentSelectedDuration
|
||||
} else {
|
||||
Log.i("[Ephemeral Messages] Configured lifetime for ephemeral messages was already $currentSelectedDuration")
|
||||
}
|
||||
|
||||
if (!chatRoom.ephemeralEnabled()) {
|
||||
Log.i("[Ephemeral Messages] Ephemeral messages were disabled, enable them")
|
||||
chatRoom.enableEphemeral(true)
|
||||
}
|
||||
} else if (chatRoom.ephemeralEnabled()) {
|
||||
Log.i("[Ephemeral Messages] Ephemeral messages were enabled, disable them")
|
||||
chatRoom.enableEphemeral(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeEphemeralDurationValues() {
|
||||
val list = arrayListOf<EphemeralDurationViewModel>()
|
||||
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_disabled, currentSelectedDuration, 0, listener))
|
||||
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_minute, currentSelectedDuration, 60, listener))
|
||||
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_hour, currentSelectedDuration, 3600, listener))
|
||||
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_day, currentSelectedDuration, 86400, listener))
|
||||
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_three_days, currentSelectedDuration, 259200, listener))
|
||||
list.add(EphemeralDurationViewModel(R.string.chat_room_ephemeral_message_one_week, currentSelectedDuration, 604800, listener))
|
||||
durationsList.value = list
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2020 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.activities.main.chat.viewmodels
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.contact.Contact
|
||||
import org.linphone.core.EventLog
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class EventViewModel(private val eventLog: EventLog) : ViewModel() {
|
||||
val text = MutableLiveData<String>()
|
||||
|
||||
val isSecurity: Boolean by lazy {
|
||||
when (eventLog.type) {
|
||||
EventLog.Type.ConferenceSecurityEvent -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private val contact: Contact? by lazy {
|
||||
val address = eventLog.participantAddress ?: eventLog.securityEventFaultyDeviceAddress
|
||||
if (address != null) {
|
||||
coreContext.contactsManager.findContactByAddress(address)
|
||||
} else {
|
||||
Log.e("[Event ViewModel] Unexpected null address for event $eventLog")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private val displayName: String by lazy {
|
||||
val address = eventLog.participantAddress ?: eventLog.securityEventFaultyDeviceAddress
|
||||
if (address != null) {
|
||||
LinphoneUtils.getDisplayName(address)
|
||||
} else {
|
||||
Log.e("[Event ViewModel] Unexpected null address for event $eventLog")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
updateEventText()
|
||||
}
|
||||
|
||||
private fun getName(): String {
|
||||
return contact?.fullName ?: displayName
|
||||
}
|
||||
|
||||
private fun updateEventText() {
|
||||
val context: Context = coreContext.context
|
||||
|
||||
text.value = when (eventLog.type) {
|
||||
EventLog.Type.ConferenceCreated -> context.getString(R.string.chat_event_conference_created)
|
||||
EventLog.Type.ConferenceTerminated -> context.getString(R.string.chat_event_conference_destroyed)
|
||||
EventLog.Type.ConferenceParticipantAdded -> context.getString(R.string.chat_event_participant_added).format(getName())
|
||||
EventLog.Type.ConferenceParticipantRemoved -> context.getString(R.string.chat_event_participant_removed).format(getName())
|
||||
EventLog.Type.ConferenceSubjectChanged -> context.getString(R.string.chat_event_subject_changed).format(eventLog.subject)
|
||||
EventLog.Type.ConferenceParticipantSetAdmin -> context.getString(R.string.chat_event_admin_set).format(getName())
|
||||
EventLog.Type.ConferenceParticipantUnsetAdmin -> context.getString(R.string.chat_event_admin_unset).format(getName())
|
||||
EventLog.Type.ConferenceParticipantDeviceAdded -> context.getString(R.string.chat_event_device_added).format(getName())
|
||||
EventLog.Type.ConferenceParticipantDeviceRemoved -> context.getString(R.string.chat_event_device_removed).format(getName())
|
||||
EventLog.Type.ConferenceSecurityEvent -> {
|
||||
val name = getName()
|
||||
when (eventLog.securityEventType) {
|
||||
EventLog.SecurityEventType.EncryptionIdentityKeyChanged -> context.getString(R.string.chat_security_event_lime_identity_key_changed).format(name)
|
||||
EventLog.SecurityEventType.ManInTheMiddleDetected -> context.getString(R.string.chat_security_event_man_in_the_middle_detected).format(name)
|
||||
EventLog.SecurityEventType.SecurityLevelDowngraded -> context.getString(R.string.chat_security_event_security_level_downgraded).format(name)
|
||||
EventLog.SecurityEventType.ParticipantMaxDeviceCountExceeded -> context.getString(R.string.chat_security_event_participant_max_count_exceeded).format(name)
|
||||
else -> "Unexpected security event for $name: ${eventLog.securityEventType}"
|
||||
}
|
||||
}
|
||||
EventLog.Type.ConferenceEphemeralMessageDisabled -> context.getString(R.string.chat_event_ephemeral_disabled)
|
||||
EventLog.Type.ConferenceEphemeralMessageEnabled -> context.getString(R.string.chat_event_ephemeral_enabled).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||
EventLog.Type.ConferenceEphemeralMessageLifetimeChanged -> context.getString(R.string.chat_event_ephemeral_lifetime_changed).format(formatEphemeralExpiration(context, eventLog.ephemeralMessageLifetime))
|
||||
else -> "Unexpected event: ${eventLog.type}"
|
||||
}
|
||||
}
|
||||
|
||||
private fun formatEphemeralExpiration(context: Context, duration: Long): String {
|
||||
return when (duration) {
|
||||
0L -> context.getString(R.string.chat_room_ephemeral_message_disabled)
|
||||
60L -> context.getString(R.string.chat_room_ephemeral_message_one_minute)
|
||||
3600L -> context.getString(R.string.chat_room_ephemeral_message_one_hour)
|
||||
86400L -> context.getString(R.string.chat_room_ephemeral_message_one_day)
|
||||
259200L -> context.getString(R.string.chat_room_ephemeral_message_three_days)
|
||||
604800L -> context.getString(R.string.chat_room_ephemeral_message_one_week)
|
||||
else -> "Unexpected duration"
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue