diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/AccountRecoverFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/AccountRecoverFragment.kt new file mode 100644 index 000000000..93b1a0bbe --- /dev/null +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/AccountRecoverFragment.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2010-2025 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.ActivityNotFoundException +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.UiThread +import androidx.core.net.toUri +import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import org.linphone.R +import org.linphone.core.tools.Log +import org.linphone.ui.GenericFragment +import org.linphone.databinding.AssistantRecoverAccountFragmentBinding +import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel +import kotlin.getValue + +@UiThread +class RecoverAccountFragment : GenericFragment() { + companion object { + private const val TAG = "[Recover Account Fragment]" + } + + private lateinit var binding: AssistantRecoverAccountFragmentBinding + + private val viewModel: AccountCreationViewModel by navGraphViewModels( + R.id.assistant_nav_graph + ) + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = AssistantRecoverAccountFragmentBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + observeToastEvents(viewModel) + + viewModel.accountRecoveryTokenReceivedEvent.observe(viewLifecycleOwner) { + it.consume { token -> + Log.i("$TAG Account recovery token received [$token], opening browser") + recoverPhoneNumberAccount(token) + } + } + + binding.setBackClickListener { + goBack() + } + + binding.setRecoverEmailAccountClickListener { + recoverEmailAccount() + } + + binding.setRecoverPhoneNumberAccountClickListener { + viewModel.requestAccountRecoveryToken() + } + } + + private fun goBack() { + findNavController().popBackStack() + } + + private fun recoverEmailAccount() { + val rootUrl = getString(R.string.web_platform_forgotten_password_url) + val url = "$rootUrl/recovery/email" + try { + Log.i("$TAG Trying to open [$url] URL") + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) + startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) + } + } + + private fun recoverPhoneNumberAccount(recoveryToken: String) { + val rootUrl = getString(R.string.web_platform_forgotten_password_url) + val url = "$rootUrl/recovery/phone/$recoveryToken" + try { + Log.i("$TAG Trying to open [$url] URL") + val browserIntent = Intent(Intent.ACTION_VIEW, url.toUri()) + startActivity(browserIntent) + } catch (ise: IllegalStateException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], IllegalStateException: $ise" + ) + } catch (anfe: ActivityNotFoundException) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url], ActivityNotFoundException: $anfe" + ) + } catch (e: Exception) { + Log.e( + "$TAG Can't start ACTION_VIEW intent for URL [$url]: $e" + ) + } + } +} 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 index 7ab107245..9ec95a50e 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/LandingFragment.kt @@ -111,8 +111,11 @@ class LandingFragment : GenericFragment() { } binding.setForgottenPasswordClickListener { - val url = getString(R.string.web_platform_forgotten_password_url) - openUrlInBrowser(url) + if (findNavController().currentDestination?.id == R.id.landingFragment) { + val action = + LandingFragmentDirections.actionLandingFragmentToRecoverAccountFragment() + findNavController().navigate(action) + } } viewModel.showPassword.observe(viewLifecycleOwner) { 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 1afb07dea..6790a7719 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 @@ -42,7 +42,6 @@ import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.AssistantRegisterFragmentBinding -import org.linphone.ui.GenericActivity import org.linphone.ui.GenericFragment import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel import org.linphone.utils.ConfirmationDialogModel @@ -164,15 +163,6 @@ class RegisterFragment : GenericFragment() { } } - viewModel.errorHappenedEvent.observe(viewLifecycleOwner) { - it.consume { error -> - (requireActivity() as GenericActivity).showRedToast( - error, - R.drawable.warning_circle - ) - } - } - val telephonyManager = requireContext().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager val countryIso = telephonyManager.networkCountryIso coreContext.postOnCoreThread { diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt index 2681eda1c..912e33967 100644 --- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt @@ -47,7 +47,6 @@ import org.linphone.core.Dictionary import org.linphone.core.Factory import org.linphone.core.tools.Log import org.linphone.ui.GenericViewModel -import org.linphone.utils.AppUtils import org.linphone.utils.Event import org.linphone.utils.LinphoneUtils @@ -105,7 +104,7 @@ class AccountCreationViewModel val accountCreatedEvent = MutableLiveData>() - val errorHappenedEvent: MutableLiveData> by lazy { + val accountRecoveryTokenReceivedEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -113,7 +112,9 @@ class AccountCreationViewModel private var waitForPushJob: Job? = null private lateinit var accountManagerServices: AccountManagerServices + private var requestedTokenIsForAccountCreation: Boolean = true private var accountCreationToken: String? = null + private var accountRecoveryToken: String? = null private var accountCreatedAuthInfo: AuthInfo? = null private var accountCreated: Account? = null @@ -124,7 +125,7 @@ class AccountCreationViewModel request: AccountManagerServicesRequest, data: String? ) { - Log.i("$TAG Request [$request] was successful, data is [$data]") + Log.i("$TAG Request [${request.type}] was successful, data is [$data]") operationInProgress.postValue(false) when (request.type) { @@ -138,6 +139,10 @@ class AccountCreationViewModel ) } } + AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush, + AccountManagerServicesRequest.Type.SendAccountRecoveryTokenByPush -> { + Log.i("$TAG Send token by push notification request has been accepted, it should be received soon") + } AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms -> { goToSmsCodeConfirmationViewEvent.postValue(Event(true)) } @@ -156,7 +161,7 @@ class AccountCreationViewModel parameterErrors: Dictionary? ) { Log.e( - "$TAG Request [$request] returned an error with status code [$statusCode] and message [$errorMessage]" + "$TAG Request [${request.type}] returned an error with status code [$statusCode] and message [$errorMessage]" ) operationInProgress.postValue(false) @@ -174,7 +179,8 @@ class AccountCreationViewModel } when (request.type) { - AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush -> { + AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush, + AccountManagerServicesRequest.Type.SendAccountRecoveryTokenByPush -> { Log.w("$TAG Cancelling job waiting for push notification") waitingForFlexiApiPushToken = false waitForPushJob?.cancel() @@ -220,11 +226,19 @@ class AccountCreationViewModel val token = customPayload.getString("token") if (token.isNotEmpty()) { - accountCreationToken = token - Log.i( - "$TAG Extracted token [$accountCreationToken] from push payload, creating account" - ) - createAccount() + if (requestedTokenIsForAccountCreation) { + accountCreationToken = token + Log.i( + "$TAG Extracted token [$accountCreationToken] from push payload, creating account" + ) + createAccount() + } else { + accountRecoveryToken = token + Log.i( + "$TAG Extracted token [$accountRecoveryToken] from push payload, opening browser" + ) + accountRecoveryTokenReceivedEvent.postValue(Event(token)) + } } else { Log.e("$TAG Push payload JSON object has an empty 'token'!") onFlexiApiTokenRequestError() @@ -317,9 +331,7 @@ class AccountCreationViewModel normalizedPhoneNumberEvent.postValue(Event(formattedPhoneNumber)) } else { Log.e("$TAG Account manager services hasn't been initialized!") - errorHappenedEvent.postValue( - Event(AppUtils.getString(R.string.assistant_account_register_unexpected_error)) - ) + showRedToast(R.string.assistant_account_register_unexpected_error, R.drawable.warning_circle) } } } @@ -330,8 +342,8 @@ class AccountCreationViewModel coreContext.postOnCoreThread { if (accountCreationToken.isNullOrEmpty()) { - Log.i("$TAG We don't have a creation token, let's request one") - requestFlexiApiToken() + Log.i("$TAG We don't have an account creation token yet, let's request one") + requestFlexiApiToken(requestAccountCreationToken = true) } else { val authInfo = accountCreatedAuthInfo if (authInfo != null) { @@ -345,6 +357,20 @@ class AccountCreationViewModel } } + @UiThread + fun requestAccountRecoveryToken() { + coreContext.postOnCoreThread { + val existingToken = accountRecoveryToken + if (existingToken.isNullOrEmpty()) { + Log.i("$TAG We don't have an account recovery token yet, let's request one") + requestFlexiApiToken(requestAccountCreationToken = false) + } else { + Log.i("$TAG We've already have a token [$existingToken], using it") + accountRecoveryTokenReceivedEvent.postValue(Event(existingToken)) + } + } + } + @UiThread fun toggleShowPassword() { showPassword.value = showPassword.value == false @@ -365,7 +391,7 @@ class AccountCreationViewModel val account = accountCreated if (::accountManagerServices.isInitialized && account != null) { val code = - "${smsCodeFirstDigit.value}${smsCodeSecondDigit.value}${smsCodeThirdDigit.value}${smsCodeLastDigit.value}" + "${smsCodeFirstDigit.value.orEmpty().trim()}${smsCodeSecondDigit.value.orEmpty().trim()}${smsCodeThirdDigit.value.orEmpty().trim()}${smsCodeLastDigit.value.orEmpty().trim()}" val identity = account.params.identityAddress if (identity != null) { Log.i( @@ -519,7 +545,8 @@ class AccountCreationViewModel } @WorkerThread - private fun requestFlexiApiToken() { + private fun requestFlexiApiToken(requestAccountCreationToken: Boolean) { + requestedTokenIsForAccountCreation = requestAccountCreationToken if (!coreContext.core.isPushNotificationAvailable) { Log.e( "$TAG Core says push notification aren't available, can't request a token from FlexiAPI" @@ -545,11 +572,21 @@ class AccountCreationViewModel } // Request an auth token, will be sent by push - val request = accountManagerServices.createSendAccountCreationTokenByPushRequest( - provider, - param, - prid - ) + val request = if (requestAccountCreationToken) { + Log.i("$TAG Requesting account creation token") + accountManagerServices.createSendAccountCreationTokenByPushRequest( + provider, + param, + prid + ) + } else { + Log.i("$TAG Requesting account recovery token") + accountManagerServices.createSendAccountRecoveryTokenByPushRequest( + provider, + param, + prid + ) + } request.addListener(accountManagerServicesListener) request.submit() @@ -580,12 +617,6 @@ class AccountCreationViewModel private fun onFlexiApiTokenRequestError() { Log.e("$TAG Flexi API token request by push error!") operationInProgress.postValue(false) - errorHappenedEvent.postValue( - Event( - AppUtils.getString( - R.string.assistant_account_register_push_notification_not_received_error - ) - ) - ) + showRedToast(R.string.assistant_account_register_push_notification_not_received_error, R.drawable.warning_circle) } } diff --git a/app/src/main/res/drawable/password.xml b/app/src/main/res/drawable/password.xml new file mode 100644 index 000000000..ee72b364f --- /dev/null +++ b/app/src/main/res/drawable/password.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/assistant_recover_account_fragment.xml b/app/src/main/res/layout/assistant_recover_account_fragment.xml new file mode 100644 index 000000000..33531c8cb --- /dev/null +++ b/app/src/main/res/layout/assistant_recover_account_fragment.xml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/assistant_nav_graph.xml b/app/src/main/res/navigation/assistant_nav_graph.xml index 6b7a5b959..17e894f0c 100644 --- a/app/src/main/res/navigation/assistant_nav_graph.xml +++ b/app/src/main/res/navigation/assistant_nav_graph.xml @@ -134,6 +134,7 @@ app:popEnterAnim="@anim/slide_in_left" app:popExitAnim="@anim/slide_out_right" app:launchSingleTop="true" /> + + app:popUpToInclusive="true" /> + - + + + \ 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 40130bbcc..ca7190ba6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -141,6 +141,12 @@ Notifications : Pour vous informer quand vous recevez un message ou un appel. Microphone : Pour permettre à vos correspondants de vous entendre. Caméra : Pour capturer votre vidéo lors des appels et des conférences. + Récupération de compte + Choisissez comment récupérer votre compte. + Vous avez créé votre compte avec : + Un email + Un numéro de téléphone + Les notifications push ne semblent pas être disponibles sur votre appareil. Celles-ci sont nécessaires à la récupération d’un compte sur l’application mobile avec un numéro de téléphone. Contacts @@ -860,7 +866,7 @@ Passer - Mot de passe oublié ? + Mot de passe oublié ou inconnu ? Passer diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3ae00480..127c587e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,7 +37,7 @@ https://linphone.org/en/privacy-policy https://linphone.org/en/terms-of-use https://subscribe.linphone.org/register/email - https://subscribe.linphone.org/ + https://subscribe.linphone.org https://weblate.linphone.org/ https://wiki.linphone.org/xwiki/wiki/public/view/Linphone/Third%20party%20components%20/#Hlinphone-android https://linphone.org/en/features/#security @@ -183,6 +183,12 @@ Post notifications: To be informed when you receive a message or a call. Record audio: So your correspondent can hear you and to record voice messages. Access camera: To capture video during video calls and conferences. + Account recovery + Choose how to recover your account. + You created your account using: + An email + A phone number + Push notifications do not seem to be available on your device, but they are mandatory for recovering a phone number account in the mobile app. Contacts @@ -903,7 +909,7 @@ Skip - Forgotten password? + Forgotten or unknown password? Skip