Added third party SIP account login form

This commit is contained in:
Sylvain Berfini 2023-09-06 15:17:42 +02:00
parent 4f03015486
commit 36b5430861
10 changed files with 541 additions and 4 deletions

View file

@ -94,11 +94,16 @@ class CorePreferences @UiThread constructor(private val context: Context) {
val linphoneDefaultValuesPath: String
get() = context.filesDir.absolutePath + "/assistant_linphone_default_values"
@get:AnyThread
val thirdPartyDefaultValuesPath: String
get() = context.filesDir.absolutePath + "/assistant_third_party_default_values"
@UiThread
fun copyAssetsFromPackage() {
copy("linphonerc_default", configPath)
copy("linphonerc_factory", factoryConfigPath, true)
copy("assistant_linphone_default_values", linphoneDefaultValuesPath, true)
copy("assistant_third_party_default_values", thirdPartyDefaultValuesPath, true)
}
@AnyThread

View file

@ -27,12 +27,16 @@ import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantLoginFragmentBinding
import org.linphone.ui.assistant.AssistantActivity
import org.linphone.ui.assistant.viewmodel.AccountLoginViewModel
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.PhoneNumberUtils
@ -97,12 +101,31 @@ class LoginFragment : GenericFragment() {
findNavController().navigate(action)
}
viewModel.showPassword.observe(viewLifecycleOwner) {
lifecycleScope.launch {
delay(50)
binding.password.setSelection(binding.password.text?.length ?: 0)
}
}
viewModel.accountLoggedInEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Account successfully logged-in, leaving assistant")
goBack()
}
}
viewModel.accountLoginErrorEvent.observe(viewLifecycleOwner) {
it.consume { message ->
Log.e("$TAG Failed to log in account [$message]")
// TODO FIXME: don't use message from callback
(requireActivity() as AssistantActivity).showRedToast(
message,
R.drawable.warning_circle
)
}
}
coreContext.postOnCoreThread {
val prefix = PhoneNumberUtils.getDeviceInternationalPrefix(requireContext())
viewModel.internationalPrefix.postValue(prefix)

View file

@ -28,8 +28,11 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
@ -119,6 +122,13 @@ class RegisterFragment : GenericFragment() {
}
}
viewModel.showPassword.observe(viewLifecycleOwner) {
lifecycleScope.launch {
delay(50)
binding.password.setSelection(binding.password.text?.length ?: 0)
}
}
viewModel.goToSmsCodeConfirmationViewEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Going to SMS code confirmation fragment")

View file

@ -23,15 +23,49 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.annotation.UiThread
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.linphone.LinphoneApplication
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantThirdPartySipAccountLoginFragmentBinding
import org.linphone.ui.assistant.AssistantActivity
import org.linphone.ui.assistant.viewmodel.ThirdPartySipAccountLoginViewModel
import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.PhoneNumberUtils
@UiThread
class ThirdPartySipAccountLoginFragment : GenericFragment() {
companion object {
private const val TAG = "[Third Party SIP Account Login Fragment]"
}
private lateinit var binding: AssistantThirdPartySipAccountLoginFragmentBinding
private val viewModel: ThirdPartySipAccountLoginViewModel by navGraphViewModels(
R.id.thirdPartySipAccountLoginFragment
)
private val dropdownListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val transport = viewModel.availableTransports[position]
Log.i("$TAG Selected transport updated [$transport]")
viewModel.transport.value = transport
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
private lateinit var adapter: ArrayAdapter<String>
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -50,8 +84,49 @@ class ThirdPartySipAccountLoginFragment : GenericFragment() {
binding.lifecycleOwner = viewLifecycleOwner
adapter = ArrayAdapter(
requireContext(),
R.layout.drop_down_item,
viewModel.availableTransports
)
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
binding.transport.adapter = adapter
binding.transport.onItemSelectedListener = dropdownListener
binding.viewModel = viewModel
binding.setBackClickListener {
goBack()
}
viewModel.showPassword.observe(viewLifecycleOwner) {
lifecycleScope.launch {
delay(50)
binding.password.setSelection(binding.password.text?.length ?: 0)
}
}
viewModel.accountLoggedInEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Account successfully logged-in, leaving assistant")
requireActivity().finish()
}
}
viewModel.accountLoginErrorEvent.observe(viewLifecycleOwner) {
it.consume { message ->
Log.e("$TAG Failed to log in account [$message]")
// TODO FIXME: don't use message from callback
(requireActivity() as AssistantActivity).showRedToast(
message,
R.drawable.warning_circle
)
}
}
coreContext.postOnCoreThread {
val prefix = PhoneNumberUtils.getDeviceInternationalPrefix(requireContext())
viewModel.internationalPrefix.postValue(prefix)
}
}
}

View file

@ -54,6 +54,8 @@ class AccountLoginViewModel @UiThread constructor() : ViewModel() {
val accountLoggedInEvent = MutableLiveData<Event<Boolean>>()
val accountLoginErrorEvent = MutableLiveData<Event<String>>()
private lateinit var newlyCreatedAuthInfo: AuthInfo
private lateinit var newlyCreatedAccount: Account
@ -78,8 +80,7 @@ class AccountLoginViewModel @UiThread constructor() : ViewModel() {
} else if (state == RegistrationState.Failed) {
registrationInProgress.postValue(false)
core.removeListener(this)
// TODO FIXME: show error
accountLoginErrorEvent.postValue(Event(message))
Log.e("$TAG Account failed to REGISTER, removing it")
core.removeAuthInfo(newlyCreatedAuthInfo)

View file

@ -0,0 +1,190 @@
/*
* Copyright (c) 2010-2023 Belledonne Communications SARL.
*
* This file is part of linphone-android
* (see https://www.linphone.org).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.assistant.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.core.Account
import org.linphone.core.AuthInfo
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.Factory
import org.linphone.core.RegistrationState
import org.linphone.core.TransportType
import org.linphone.core.tools.Log
import org.linphone.utils.Event
class ThirdPartySipAccountLoginViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Third Party SIP Account Login ViewModel]"
private const val UDP = "UDP"
private const val TCP = "TCP"
private const val TLS = "TLS"
}
val username = MutableLiveData<String>()
val password = MutableLiveData<String>()
val domain = MutableLiveData<String>()
val displayName = MutableLiveData<String>()
val transport = MutableLiveData<String>()
val internationalPrefix = MutableLiveData<String>()
val showPassword = MutableLiveData<Boolean>()
val loginEnabled = MediatorLiveData<Boolean>()
val registrationInProgress = MutableLiveData<Boolean>()
val accountLoggedInEvent = MutableLiveData<Event<Boolean>>()
val accountLoginErrorEvent = MutableLiveData<Event<String>>()
val availableTransports = arrayListOf<String>()
private lateinit var newlyCreatedAuthInfo: AuthInfo
private lateinit var newlyCreatedAccount: Account
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onAccountRegistrationStateChanged(
core: Core,
account: Account,
state: RegistrationState?,
message: String
) {
if (account == newlyCreatedAccount) {
Log.i("$TAG Newly created account registration state is [$state] ($message)")
if (state == RegistrationState.Ok) {
registrationInProgress.postValue(false)
core.removeListener(this)
// Set new account as default
core.defaultAccount = newlyCreatedAccount
accountLoggedInEvent.postValue(Event(true))
} else if (state == RegistrationState.Failed) {
registrationInProgress.postValue(false)
core.removeListener(this)
accountLoginErrorEvent.postValue(Event(message))
Log.e("$TAG Account failed to REGISTER, removing it")
core.removeAuthInfo(newlyCreatedAuthInfo)
core.removeAccount(newlyCreatedAccount)
}
}
}
}
init {
showPassword.value = false
registrationInProgress.value = false
loginEnabled.addSource(username) {
loginEnabled.value = isLoginButtonEnabled()
}
loginEnabled.addSource(password) {
loginEnabled.value = isLoginButtonEnabled()
}
loginEnabled.addSource(domain) {
loginEnabled.value = isLoginButtonEnabled()
}
availableTransports.add(UDP)
availableTransports.add(TCP)
availableTransports.add(TLS)
transport.value = UDP
}
@UiThread
fun login() {
coreContext.postOnCoreThread { core ->
core.loadConfigFromXml(corePreferences.thirdPartyDefaultValuesPath)
val user = username.value.orEmpty()
val domainValue = domain.value.orEmpty()
newlyCreatedAuthInfo = Factory.instance().createAuthInfo(
user,
null,
password.value.orEmpty(),
null,
null,
domainValue
)
core.addAuthInfo(newlyCreatedAuthInfo)
val accountParams = core.createAccountParams()
val identityAddress = Factory.instance().createAddress("sip:$user@$domainValue")
if (displayName.value.orEmpty().isNotEmpty()) {
identityAddress?.displayName = displayName.value.orEmpty()
}
accountParams.identityAddress = identityAddress
val serverAddress = Factory.instance().createAddress("sip:$domainValue")
serverAddress?.transport = when (transport.value.orEmpty()) {
TCP -> TransportType.Tcp
TLS -> TransportType.Tls
else -> TransportType.Udp
}
accountParams.serverAddress = serverAddress
val prefix = internationalPrefix.value.orEmpty()
if (prefix.isNotEmpty()) {
val prefixDigits = if (prefix.startsWith("+")) {
prefix.substring(1)
} else {
prefix
}
if (prefixDigits.isNotEmpty()) {
Log.i("$TAG Setting international prefix [$prefixDigits] in account params")
accountParams.internationalPrefix = prefixDigits
}
}
newlyCreatedAccount = core.createAccount(accountParams)
registrationInProgress.postValue(true)
core.addListener(coreListener)
core.addAccount(newlyCreatedAccount)
}
}
@UiThread
fun toggleShowPassword() {
showPassword.value = showPassword.value == false
}
@UiThread
private fun isLoginButtonEnabled(): Boolean {
return username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty()
}
}

View file

@ -100,7 +100,7 @@
android:textSize="14sp"
android:textColor="@color/gray_9"
android:background="@{viewModel.usernameError.length() > 0 ? @drawable/shape_edit_text_error_background : @drawable/edit_text_background, default=@drawable/edit_text_background}"
android:inputType=""
android:inputType="text"
android:hint="Username"
app:layout_constraintTop_toBottomOf="@id/username_label"
app:layout_constraintStart_toStartOf="parent"
@ -130,7 +130,6 @@
android:text="Phone Number*"
android:textSize="13sp"
android:textColor="@color/gray_9"
android:inputType="text"
app:layout_constraintTop_toBottomOf="@id/username_error"
app:layout_constraintStart_toStartOf="parent"/>

View file

@ -9,6 +9,9 @@
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.assistant.viewmodel.ThirdPartySipAccountLoginViewModel" />
</data>
<ScrollView
@ -32,6 +35,224 @@
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:text="Use a third party SIP Account"
android:textSize="20sp"
android:textColor="@color/gray_9"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/username_label"
style="@style/default_text_style_700"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="38dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="Username*"
android:textSize="13sp"
android:textColor="@color/gray_9"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@={viewModel.username, default=`johndoe`}"
android:textSize="14sp"
android:textColor="@color/gray_9"
android:background="@drawable/edit_text_background"
android:inputType="text"
android:hint="Username"
app:layout_constraintTop_toBottomOf="@id/username_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/password_label"
style="@style/default_text_style_700"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="16dp"
android:text="Password*"
android:textSize="13sp"
android:textColor="@color/gray_9"
app:layout_constraintTop_toBottomOf="@id/username"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/password"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@={viewModel.password, default=`johndoe`}"
android:textSize="14sp"
android:textColor="@color/gray_9"
android:background="@drawable/edit_text_background"
android:hint="Password"
android:inputType="@{viewModel.showPassword ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD, default=textPassword}"
app:layout_constraintTop_toBottomOf="@id/password_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:onClick="@{() -> viewModel.toggleShowPassword()}"
android:id="@+id/eye"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:layout_marginEnd="20dp"
android:padding="4dp"
android:src="@{viewModel.showPassword ? @drawable/eye_slash : @drawable/eye, default=@drawable/eye}"
app:tint="@color/gray_1"
app:layout_constraintEnd_toEndOf="@id/password"
app:layout_constraintTop_toTopOf="@id/password"
app:layout_constraintBottom_toBottomOf="@id/password" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/domain_label"
style="@style/default_text_style_700"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="Domain*"
android:textSize="13sp"
android:textColor="@color/gray_9"
app:layout_constraintTop_toBottomOf="@id/password"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/domain"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@={viewModel.domain, default=`sip.example.net`}"
android:textSize="14sp"
android:textColor="@color/gray_9"
android:background="@drawable/edit_text_background"
android:inputType="text|textUri"
android:hint="Domain"
app:layout_constraintTop_toBottomOf="@id/domain_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/display_name_label"
style="@style/default_text_style_700"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="Display Name"
android:textSize="13sp"
android:textColor="@color/gray_9"
app:layout_constraintTop_toBottomOf="@id/domain"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/display_name"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@={viewModel.displayName, default=`John Doe`}"
android:textSize="14sp"
android:textColor="@color/gray_9"
android:background="@drawable/edit_text_background"
android:inputType="text|textPersonName"
android:hint="Display Name"
app:layout_constraintTop_toBottomOf="@id/display_name_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/transport_label"
style="@style/default_text_style_700"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="18dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="Transport*"
android:textSize="13sp"
android:textColor="@color/gray_9"
app:layout_constraintTop_toBottomOf="@id/display_name"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatSpinner
android:id="@+id/transport"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:background="@drawable/edit_text_background"
app:layout_constraintTop_toBottomOf="@id/transport_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/transport_caret"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:src="@drawable/caret_down"
app:layout_constraintTop_toTopOf="@id/transport"
app:layout_constraintBottom_toBottomOf="@id/transport"
app:layout_constraintEnd_toEndOf="@id/transport"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.login()}"
android:enabled="@{viewModel.loginEnabled &amp;&amp; !viewModel.registrationInProgress, default=false}"
style="@style/default_text_style_600"
android:id="@+id/login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingTop="@dimen/primary_secondary_buttons_label_padding"
android:paddingBottom="@dimen/primary_secondary_buttons_label_padding"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="Login"
android:textSize="18sp"
android:textColor="@color/primary_button_label_color"
android:gravity="center"
android:background="@drawable/primary_button_background"
app:layout_constraintTop_toBottomOf="@id/transport"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView
xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/default_text_style"
android:layout_width="match_parent"
android:layout_height="64dp"
android:layout_marginTop="15dp"
android:layout_marginBottom="15dp"
android:textSize="14sp"
android:textColor="@color/gray_9"
android:gravity="center_vertical"
android:singleLine="true"
android:ellipsize="marquee"/>