diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt new file mode 100644 index 000000000..d2b2dc226 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.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.fragment + +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.UiThread +import androidx.fragment.app.Fragment +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.R +import org.linphone.core.tools.Log +import org.linphone.databinding.AssistantLandingFragmentBinding +import org.linphone.ui.assistant.model.AcceptConditionsAndPolicyDialogModel +import org.linphone.ui.assistant.viewmodel.LandingViewModel +import org.linphone.ui.sso.OpenIdActivity +import org.linphone.utils.DialogUtils + +@UiThread +class LandingFragment : Fragment() { + companion object { + private const val TAG = "[Landing Fragment]" + } + + private lateinit var binding: AssistantLandingFragmentBinding + + private val viewModel: LandingViewModel by navGraphViewModels( + R.id.assistant_nav_graph + ) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantLandingFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + + binding.setBackClickListener { + requireActivity().finish() + } + + binding.setRegisterClickListener { + if (viewModel.conditionsAndPrivacyPolicyAccepted) { + goToRegisterFragment() + } else { + showAcceptConditionsAndPrivacyDialog(goToAccountCreate = true) + } + } + + binding.setQrCodeClickListener { + val action = LandingFragmentDirections.actionLandingFragmentToQrCodeScannerFragment() + findNavController().navigate(action) + } + + binding.setThirdPartySipAccountLoginClickListener { + if (viewModel.conditionsAndPrivacyPolicyAccepted) { + goToLoginThirdPartySipAccountFragment() + } else { + showAcceptConditionsAndPrivacyDialog(goToThirdPartySipAccountLogin = true) + } + } + + viewModel.redirectToDigestAuthEvent.observe(viewLifecycleOwner) { + it.consume { + goToLoginFragment() + } + } + + viewModel.redirectToSingleSignOnEvent.observe(viewLifecycleOwner) { + it.consume { + goToSingleSignOnActivity() + } + } + } + + private fun goToLoginFragment() { + val identity = viewModel.sipIdentity.value.orEmpty() + val action = LandingFragmentDirections.actionLandingFragmentToLoginFragment(identity) + findNavController().navigate(action) + } + + private fun goToSingleSignOnActivity() { + startActivity(Intent(requireContext(), OpenIdActivity::class.java)) + requireActivity().finish() + } + + private fun goToRegisterFragment() { + val action = LandingFragmentDirections.actionLandingFragmentToRegisterFragment() + findNavController().navigate(action) + } + + private fun goToLoginThirdPartySipAccountFragment() { + val action = LandingFragmentDirections.actionLandingFragmentToThirdPartySipAccountWarningFragment() + findNavController().navigate(action) + } + + private fun showAcceptConditionsAndPrivacyDialog( + goToAccountCreate: Boolean = false, + goToThirdPartySipAccountLogin: Boolean = false + ) { + val model = AcceptConditionsAndPolicyDialogModel() + val dialog = DialogUtils.getAcceptConditionsAndPrivacyDialog( + requireActivity(), + model + ) + + model.dismissEvent.observe(viewLifecycleOwner) { + it.consume { + dialog.dismiss() + } + } + + model.conditionsAcceptedEvent.observe(viewLifecycleOwner) { + it.consume { + Log.i("$TAG Conditions & Privacy policy have been accepted") + coreContext.postOnCoreThread { + corePreferences.conditionsAndPrivacyPolicyAccepted = true + } + dialog.dismiss() + + if (goToAccountCreate) { + goToRegisterFragment() + } else if (goToThirdPartySipAccountLogin) { + goToLoginThirdPartySipAccountFragment() + } + } + } + + model.privacyPolicyClickedEvent.observe(viewLifecycleOwner) { + it.consume { + val url = getString(R.string.website_privacy_policy_url) + try { + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } + } + } + + model.generalTermsClickedEvent.observe(viewLifecycleOwner) { + it.consume { + val url = getString(R.string.website_terms_and_conditions_url) + try { + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } + } + } + + dialog.show() + } +} 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 f651242a9..fcc49e127 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 @@ -29,19 +29,16 @@ import androidx.annotation.UiThread import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController +import androidx.navigation.fragment.navArgs import androidx.navigation.navGraphViewModels import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext -import org.linphone.LinphoneApplication.Companion.corePreferences 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.model.AcceptConditionsAndPolicyDialogModel import org.linphone.ui.assistant.viewmodel.AccountLoginViewModel -import org.linphone.ui.sso.OpenIdActivity -import org.linphone.utils.DialogUtils import org.linphone.utils.PhoneNumberUtils @UiThread @@ -52,6 +49,8 @@ class LoginFragment : Fragment() { private lateinit var binding: AssistantLoginFragmentBinding + private val args: LoginFragmentArgs by navArgs() + private val viewModel: AccountLoginViewModel by navGraphViewModels( R.id.assistant_nav_graph ) @@ -72,16 +71,11 @@ class LoginFragment : Fragment() { binding.viewModel = viewModel binding.setBackClickListener { - requireActivity().finish() + goBack() } - binding.setRegisterClickListener { - if (viewModel.conditionsAndPrivacyPolicyAccepted) { - goToRegisterFragment() - } else { - showAcceptConditionsAndPrivacyDialog(goToAccountCreate = true) - } - } + val identity = args.sipIdentity + viewModel.sipIdentity.value = identity binding.setForgottenPasswordClickListener { val url = getString(R.string.web_platform_forgotten_password_url) @@ -95,24 +89,6 @@ class LoginFragment : Fragment() { } } - binding.setSingleSignOnClickListener { - startActivity(Intent(requireContext(), OpenIdActivity::class.java)) - requireActivity().finish() - } - - binding.setQrCodeClickListener { - val action = LoginFragmentDirections.actionLoginFragmentToQrCodeScannerFragment() - findNavController().navigate(action) - } - - binding.setThirdPartySipAccountLoginClickListener { - if (viewModel.conditionsAndPrivacyPolicyAccepted) { - goToLoginThirdPartySipAccountFragment() - } else { - showAcceptConditionsAndPrivacyDialog(goToThirdPartySipAccountLogin = true) - } - } - viewModel.showPassword.observe(viewLifecycleOwner) { lifecycleScope.launch { delay(50) @@ -146,76 +122,7 @@ class LoginFragment : Fragment() { } } - private fun goToRegisterFragment() { - val action = LoginFragmentDirections.actionLoginFragmentToRegisterFragment() - findNavController().navigate(action) - } - - private fun goToLoginThirdPartySipAccountFragment() { - val action = LoginFragmentDirections.actionLoginFragmentToThirdPartySipAccountWarningFragment() - findNavController().navigate(action) - } - - private fun showAcceptConditionsAndPrivacyDialog( - goToAccountCreate: Boolean = false, - goToThirdPartySipAccountLogin: Boolean = false - ) { - val model = AcceptConditionsAndPolicyDialogModel() - val dialog = DialogUtils.getAcceptConditionsAndPrivacyDialog( - requireActivity(), - model - ) - - model.dismissEvent.observe(viewLifecycleOwner) { - it.consume { - dialog.dismiss() - } - } - - model.conditionsAcceptedEvent.observe(viewLifecycleOwner) { - it.consume { - Log.i("$TAG Conditions & Privacy policy have been accepted") - coreContext.postOnCoreThread { - corePreferences.conditionsAndPrivacyPolicyAccepted = true - } - dialog.dismiss() - - if (goToAccountCreate) { - goToRegisterFragment() - } else if (goToThirdPartySipAccountLogin) { - goToLoginThirdPartySipAccountFragment() - } - } - } - - model.privacyPolicyClickedEvent.observe(viewLifecycleOwner) { - it.consume { - val url = getString(R.string.website_privacy_policy_url) - try { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } - } - } - - model.generalTermsClickedEvent.observe(viewLifecycleOwner) { - it.consume { - val url = getString(R.string.website_terms_and_conditions_url) - try { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(browserIntent) - } catch (ise: IllegalStateException) { - Log.e( - "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" - ) - } - } - } - - dialog.show() + private fun goBack() { + findNavController().popBackStack() } } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt index 32333f268..3f6ffb823 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/PermissionsFragment.kt @@ -113,7 +113,7 @@ class PermissionsFragment : Fragment() { } private fun goToLoginFragment() { - val action = PermissionsFragmentDirections.actionPermissionsFragmentToLoginFragment() + val action = PermissionsFragmentDirections.actionPermissionsFragmentToLandingFragment() findNavController().navigate(action) } diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt index a77590d25..d73d86141 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt @@ -68,7 +68,10 @@ class RegisterCodeConfirmationFragment : Fragment() { viewModel.goToLoginPageEvent.observe(viewLifecycleOwner) { it.consume { Log.i("$TAG Going to login fragment") - val action = RegisterCodeConfirmationFragmentDirections.actionRegisterCodeConfirmationFragmentToLoginFragment() + val identity = viewModel.username.value.orEmpty() + val action = RegisterCodeConfirmationFragmentDirections.actionRegisterCodeConfirmationFragmentToLoginFragment( + identity + ) findNavController().navigate(action) } } 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 19f034013..cb93e9e30 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 @@ -43,9 +43,7 @@ class AccountLoginViewModel @UiThread constructor() : ViewModel() { private const val TAG = "[Account Login ViewModel]" } - val showBackButton = MutableLiveData() - - val username = MutableLiveData() + val sipIdentity = MutableLiveData() val password = MutableLiveData() @@ -69,8 +67,6 @@ class AccountLoginViewModel @UiThread constructor() : ViewModel() { MutableLiveData>() } - var conditionsAndPrivacyPolicyAccepted = false - private lateinit var newlyCreatedAuthInfo: AuthInfo private lateinit var newlyCreatedAccount: Account @@ -121,13 +117,7 @@ class AccountLoginViewModel @UiThread constructor() : ViewModel() { showPassword.value = false registrationInProgress.value = false - coreContext.postOnCoreThread { core -> - // Prevent user from leaving assistant if no account was configured yet - showBackButton.postValue(core.accountList.isNotEmpty()) - conditionsAndPrivacyPolicyAccepted = corePreferences.conditionsAndPrivacyPolicyAccepted - } - - loginEnabled.addSource(username) { + loginEnabled.addSource(sipIdentity) { loginEnabled.value = isLoginButtonEnabled() } loginEnabled.addSource(password) { @@ -142,8 +132,14 @@ class AccountLoginViewModel @UiThread constructor() : ViewModel() { coreContext.postOnCoreThread { core -> core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath) - val user = username.value.orEmpty().trim() - val domain = corePreferences.defaultDomain + val identity = sipIdentity.value.orEmpty().trim() + val identityAddress = core.interpretUrl(identity, false) + identityAddress ?: return@postOnCoreThread + + val user = identityAddress.username + user ?: return@postOnCoreThread + + val domain = identityAddress.domain newlyCreatedAuthInfo = Factory.instance().createAuthInfo( user, @@ -156,20 +152,6 @@ class AccountLoginViewModel @UiThread constructor() : ViewModel() { core.addAuthInfo(newlyCreatedAuthInfo) val accountParams = core.createAccountParams() - 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) accountParams.identityAddress = identityAddress val prefix = internationalPrefix.value.orEmpty().trim() @@ -214,6 +196,6 @@ class AccountLoginViewModel @UiThread constructor() : ViewModel() { @UiThread private fun isLoginButtonEnabled(): Boolean { - return username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() + return sipIdentity.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() } } diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/LandingViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/LandingViewModel.kt new file mode 100644 index 000000000..4e70cf455 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/LandingViewModel.kt @@ -0,0 +1,81 @@ +/* + * 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.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.core.Factory +import org.linphone.utils.Event + +class LandingViewModel @UiThread constructor() : ViewModel() { + companion object { + private const val TAG = "[Account Login ViewModel]" + } + + val showBackButton = MutableLiveData() + + val sipIdentity = MutableLiveData() + + val redirectToDigestAuthEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + val redirectToSingleSignOnEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + + var conditionsAndPrivacyPolicyAccepted = false + + init { + coreContext.postOnCoreThread { core -> + // Prevent user from leaving assistant if no account was configured yet + showBackButton.postValue(core.accountList.isNotEmpty()) + conditionsAndPrivacyPolicyAccepted = corePreferences.conditionsAndPrivacyPolicyAccepted + } + } + + @UiThread + fun login() { + coreContext.postOnCoreThread { core -> + var identity = sipIdentity.value.orEmpty() + if (!identity.startsWith("sip:")) { + identity = "sip:$identity" + } + if (!identity.contains("@")) { + identity = "$identity@${corePreferences.defaultDomain}" + } + val identityAddress = Factory.instance().createAddress(identity) + if (identityAddress == null) { + // TODO: FIXME: show error + return@postOnCoreThread + } + + // TODO: SSO or password auth? + if (identityAddress.domain == corePreferences.defaultDomain) { + redirectToDigestAuthEvent.postValue(Event(true)) + } else { + redirectToSingleSignOnEvent.postValue(Event(true)) + } + } + } +} diff --git a/app/src/main/res/layout/assistant_landing_fragment.xml b/app/src/main/res/layout/assistant_landing_fragment.xml new file mode 100644 index 000000000..8abba59d0 --- /dev/null +++ b/app/src/main/res/layout/assistant_landing_fragment.xml @@ -0,0 +1,233 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/assistant_login_fragment.xml b/app/src/main/res/layout/assistant_login_fragment.xml index cf8945bb3..66ada8d80 100644 --- a/app/src/main/res/layout/assistant_login_fragment.xml +++ b/app/src/main/res/layout/assistant_login_fragment.xml @@ -8,21 +8,9 @@ - - - - @@ -46,7 +34,6 @@ android:padding="15dp" android:src="@drawable/caret_left" android:contentDescription="@string/content_description_go_back_icon" - android:visibility="@{viewModel.showBackButton ? View.VISIBLE : View.INVISIBLE, default=invisible}" app:tint="?attr/color_main2_500" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"/> @@ -79,32 +66,32 @@ + app:layout_constraintStart_toStartOf="@id/sip_identity"/> @@ -115,9 +102,9 @@ android:layout_height="wrap_content" android:layout_marginEnd="16dp" android:layout_marginTop="16dp" - android:text="@{@string/password + `*`}" + android:text="@{@string/password + `*`, default=@string/password}" app:layout_constraintWidth_max="@dimen/text_input_max_width" - app:layout_constraintTop_toBottomOf="@id/username" + app:layout_constraintTop_toBottomOf="@id/sip_identity" app:layout_constraintStart_toStartOf="@id/password"/> - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/assistant_nav_graph.xml b/app/src/main/res/navigation/assistant_nav_graph.xml index 20157b639..f217e088b 100644 --- a/app/src/main/res/navigation/assistant_nav_graph.xml +++ b/app/src/main/res/navigation/assistant_nav_graph.xml @@ -3,7 +3,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/assistant_nav_graph" - app:startDestination="@id/loginFragment"> + app:startDestination="@id/landingFragment"> - - - - - + @@ -137,14 +114,64 @@ android:name="org.linphone.ui.assistant.fragment.PermissionsFragment" android:label="PermissionsFragment" tools:layout="@layout/assistant_permissions_fragment"> + + + + + + + + + - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 570639f33..5d374ec43 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ Dummy subject SIP address + username@domain Display name Domain Username