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