From 36b54308612b53dc56450ce56edca53c2ec632a7 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 6 Sep 2023 15:17:42 +0200 Subject: [PATCH] Added third party SIP account login form --- ...s => assistant_third_party_default_values} | 0 .../java/org/linphone/core/CorePreferences.kt | 5 + .../ui/assistant/fragment/LoginFragment.kt | 23 ++ .../ui/assistant/fragment/RegisterFragment.kt | 10 + .../ThirdPartySipAccountLoginFragment.kt | 75 ++++++ .../viewmodel/AccountLoginViewModel.kt | 5 +- .../ThirdPartySipAccountLoginViewModel.kt | 190 +++++++++++++++ .../layout/assistant_register_fragment.xml | 3 +- ...third_party_sip_account_login_fragment.xml | 221 ++++++++++++++++++ app/src/main/res/layout/drop_down_item.xml | 13 ++ 10 files changed, 541 insertions(+), 4 deletions(-) rename app/src/main/assets/{assistant_default_values => assistant_third_party_default_values} (100%) create mode 100644 app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt create mode 100644 app/src/main/res/layout/drop_down_item.xml diff --git a/app/src/main/assets/assistant_default_values b/app/src/main/assets/assistant_third_party_default_values similarity index 100% rename from app/src/main/assets/assistant_default_values rename to app/src/main/assets/assistant_third_party_default_values diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 224099d51..75e320dc9 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -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 diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt index 586f98df6..0780cfcfa 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt @@ -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) diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt index 9d8bc0889..f478d8f03 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt @@ -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") diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt index 3fb4e4cde..ded2b1265 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/ThirdPartySipAccountLoginFragment.kt @@ -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 + 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) + } } } diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountLoginViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountLoginViewModel.kt index 9ee3546c4..8bf9dcd13 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountLoginViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountLoginViewModel.kt @@ -54,6 +54,8 @@ class AccountLoginViewModel @UiThread constructor() : ViewModel() { val accountLoggedInEvent = MutableLiveData>() + val accountLoginErrorEvent = MutableLiveData>() + 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) diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt new file mode 100644 index 000000000..92957a6bc --- /dev/null +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt @@ -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 . + */ +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() + + val password = MutableLiveData() + + val domain = MutableLiveData() + + val displayName = MutableLiveData() + + val transport = MutableLiveData() + + val internationalPrefix = MutableLiveData() + + val showPassword = MutableLiveData() + + val loginEnabled = MediatorLiveData() + + val registrationInProgress = MutableLiveData() + + val accountLoggedInEvent = MutableLiveData>() + + val accountLoginErrorEvent = MutableLiveData>() + + val availableTransports = arrayListOf() + + 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() + } +} diff --git a/app/src/main/res/layout/assistant_register_fragment.xml b/app/src/main/res/layout/assistant_register_fragment.xml index eb66470e9..99ad26ad2 100644 --- a/app/src/main/res/layout/assistant_register_fragment.xml +++ b/app/src/main/res/layout/assistant_register_fragment.xml @@ -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"/> diff --git a/app/src/main/res/layout/assistant_third_party_sip_account_login_fragment.xml b/app/src/main/res/layout/assistant_third_party_sip_account_login_fragment.xml index cd10bee91..2044c157e 100644 --- a/app/src/main/res/layout/assistant_third_party_sip_account_login_fragment.xml +++ b/app/src/main/res/layout/assistant_third_party_sip_account_login_fragment.xml @@ -9,6 +9,9 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/drop_down_item.xml b/app/src/main/res/layout/drop_down_item.xml new file mode 100644 index 000000000..c887dd6cd --- /dev/null +++ b/app/src/main/res/layout/drop_down_item.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file