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 d9d3d3d20..1fcdf275a 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 @@ -38,6 +38,7 @@ import org.linphone.databinding.AssistantThirdPartySipAccountLoginFragmentBindin import org.linphone.ui.GenericActivity import org.linphone.ui.GenericFragment import org.linphone.ui.assistant.viewmodel.ThirdPartySipAccountLoginViewModel +import org.linphone.ui.main.sso.fragment.SingleSignOnFragmentDirections import org.linphone.utils.PhoneNumberUtils @UiThread @@ -119,6 +120,22 @@ class ThirdPartySipAccountLoginFragment : GenericFragment() { } } + coreContext.bearerAuthenticationRequestedEvent.observe(viewLifecycleOwner) { + it.consume { pair -> + val serverUrl = pair.first + val username = pair.second + + Log.i( + "$TAG Navigating to Single Sign On Fragment with server URL [$serverUrl] and username [$username]" + ) + val action = SingleSignOnFragmentDirections.actionGlobalSingleSignOnFragment( + serverUrl, + username + ) + findNavController().navigate(action) + } + } + coreContext.postOnCoreThread { val dialPlan = PhoneNumberUtils.getDeviceDialPlan(requireContext()) if (dialPlan != null) { 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 index 680b4ed65..d3442b06c 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/ThirdPartySipAccountLoginViewModel.kt @@ -124,9 +124,6 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : GenericViewMo loginEnabled.addSource(username) { loginEnabled.value = isLoginButtonEnabled() } - loginEnabled.addSource(password) { - loginEnabled.value = isLoginButtonEnabled() - } loginEnabled.addSource(domain) { loginEnabled.value = isLoginButtonEnabled() } @@ -143,8 +140,31 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : GenericViewMo coreContext.postOnCoreThread { core -> core.loadConfigFromXml(corePreferences.thirdPartyDefaultValuesPath) - val user = username.value.orEmpty().trim() + // Remove sip: in front of domain, just in case... val domainValue = domain.value.orEmpty().trim() + val domain = if (domainValue.startsWith("sip:")) { + domainValue.substring("sip:".length) + } else { + domainValue + } + + // Allow to enter SIP identity instead of simply username + // in case identity domain doesn't match proxy domain + val user = username.value.orEmpty().trim() + val identity = if (user.startsWith("sip:")) { + if (user.contains("@")) { + user + } else { + "$user@$domain" + } + } else { + if (user.contains("@")) { + "sip:$user" + } else { + "sip:$user@$domain" + } + } + val identityAddress = Factory.instance().createAddress(identity) newlyCreatedAuthInfo = Factory.instance().createAuthInfo( user, @@ -152,19 +172,18 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : GenericViewMo password.value.orEmpty().trim(), null, null, - domainValue + null ) 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().trim() } accountParams.identityAddress = identityAddress - val serverAddress = Factory.instance().createAddress("sip:$domainValue") + val serverAddress = Factory.instance().createAddress("sip:$domain") serverAddress?.transport = when (transport.value.orEmpty().trim()) { TransportType.Tcp.name.uppercase(Locale.getDefault()) -> TransportType.Tcp TransportType.Tls.name.uppercase(Locale.getDefault()) -> TransportType.Tls @@ -204,6 +223,7 @@ class ThirdPartySipAccountLoginViewModel @UiThread constructor() : GenericViewMo @UiThread private fun isLoginButtonEnabled(): Boolean { - return username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() + // Password isn't mandatory as authentication could be Bearer + return username.value.orEmpty().isNotEmpty() && domain.value.orEmpty().isNotEmpty() } } diff --git a/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt b/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt index e95c852c0..45a02a6d4 100644 --- a/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/sso/fragment/SingleSignOnFragment.kt @@ -24,8 +24,8 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.lifecycle.ViewModelProvider import androidx.navigation.fragment.navArgs -import androidx.navigation.navGraphViewModels import net.openid.appauth.AuthorizationException import net.openid.appauth.AuthorizationResponse import org.linphone.R @@ -44,9 +44,7 @@ class SingleSignOnFragment : GenericMainFragment() { private lateinit var binding: SingleSignOnFragmentBinding - private val viewModel: SingleSignOnViewModel by navGraphViewModels( - R.id.main_nav_graph - ) + private lateinit var viewModel: SingleSignOnViewModel private val args: SingleSignOnFragmentArgs by navArgs() @@ -62,6 +60,8 @@ class SingleSignOnFragment : GenericMainFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.lifecycleOwner = viewLifecycleOwner + + viewModel = ViewModelProvider(this)[SingleSignOnViewModel::class.java] binding.viewModel = viewModel observeToastEvents(viewModel) diff --git a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt index b79c9d4e5..a51956f29 100644 --- a/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/sso/viewmodel/SingleSignOnViewModel.kt @@ -114,6 +114,7 @@ class SingleSignOnViewModel : GenericViewModel() { "$singleSignOnUrl/.well-known/openid-configuration" } singleSignOn() + return@RetrieveConfigurationCallback } else { onErrorEvent.postValue(Event("Failed to fetch configuration")) return@RetrieveConfigurationCallback @@ -158,34 +159,48 @@ class SingleSignOnViewModel : GenericViewModel() { authService = AuthorizationService(coreContext.context) } + val authStateJsonFile = File( + coreContext.context.filesDir.absolutePath, + "auth_state.json" + ) Log.i("$TAG Starting refresh token request") - authService.performTokenRequest( - authState.createTokenRefreshRequest() - ) { resp, ex -> - if (resp != null) { - Log.i("$TAG Token refresh succeeded!") + try { + authService.performTokenRequest( + authState.createTokenRefreshRequest() + ) { resp, ex -> + if (resp != null) { + Log.i("$TAG Token refresh succeeded!") - if (::authState.isInitialized) { - Log.i("$TAG Updating AuthState object after refresh token response") - authState.update(resp, ex) - storeAuthStateAsJsonFile() - } - updateTokenInfo() - } else { - Log.e( - "$TAG Failed to perform token refresh [$ex], destroying auth_state.json file" - ) - onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) - - val file = File(coreContext.context.filesDir.absolutePath, "auth_state.json") - viewModelScope.launch { - FileUtils.deleteFile(file.absolutePath) - Log.w( - "$TAG Previous auth_state.json file deleted, starting single sign on process from scratch" + if (::authState.isInitialized) { + Log.i("$TAG Updating AuthState object after refresh token response") + authState.update(resp, ex) + storeAuthStateAsJsonFile() + } + updateTokenInfo() + } else { + Log.e( + "$TAG Failed to perform token refresh [$ex], destroying auth_state.json file" ) - singleSignOn() + onErrorEvent.postValue(Event(ex?.errorDescription.orEmpty())) + + viewModelScope.launch { + FileUtils.deleteFile(authStateJsonFile.absolutePath) + Log.w( + "$TAG Previous auth_state.json file deleted, starting single sign on process from scratch" + ) + singleSignOn() + } } } + } catch (ise: IllegalStateException) { + Log.e("$TAG Illegal state exception, clearing auth state and trying again: $ise") + viewModelScope.launch { + FileUtils.deleteFile(authStateJsonFile.absolutePath) + authState = getAuthState() + performRefreshToken() + } + } catch (e: Exception) { + Log.e("$TAG Failed to perform token request: $e") } } } 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 51567f51d..4696816b9 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 @@ -70,7 +70,7 @@ android:layout_marginTop="38dp" android:layout_marginStart="16dp" android:layout_marginEnd="16dp" - android:text="@{@string/username + `*`, default=`Username*`}" + android:text="@string/assistant_third_party_sip_account_username_or_identity" app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="@id/username"/> diff --git a/app/src/main/res/navigation/assistant_nav_graph.xml b/app/src/main/res/navigation/assistant_nav_graph.xml index c5c79e34e..d5f86e01c 100644 --- a/app/src/main/res/navigation/assistant_nav_graph.xml +++ b/app/src/main/res/navigation/assistant_nav_graph.xml @@ -168,4 +168,27 @@ app:destination="@id/profileModeFragment" /> + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0d16c5500..095ac29cf 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -137,6 +137,7 @@ Certaines fonctionalités telles que les conversations de groupe, les vidéo-conférences, etc… nécessitent un compte &appName;.\n\nCes fonctionalités seront masquées si vous utilisez un compte SIP tiers.\n\nPour les activer dans un projet commercial, merci de nous contacter. Je préfère créer un compte J\'ai compris + Nom d\'utilisateur ou identité SIP* Choisissez votre mode Vous pourrez le changer plus tard Continuer diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bedba33e0..a423827bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -172,6 +172,7 @@ Some features require a &appName; account, such as group messaging, video conferences…\n\nThese features are hidden when you register with a third party SIP account.\n\nTo enable it in a commercial project, please contact us. I prefer to create an account I understand + Username or SIP identity* Personalize your profile mode You may change that mode later Continue