Added recover phone account form to be able to login with only a phone number

This commit is contained in:
Sylvain Berfini 2025-04-24 16:34:02 +02:00
parent 1843bd4c1e
commit cf513627ab
15 changed files with 1524 additions and 20 deletions

View file

@ -49,6 +49,7 @@ record_aware=1
[account_creator]
url=https://subscribe.linphone.org/api/
backend=1
[lime]
lime_update_threshold=86400

View file

@ -103,22 +103,10 @@ class LandingFragment : GenericFragment() {
}
binding.setForgottenPasswordClickListener {
val url = getString(R.string.web_platform_forgotten_password_url)
try {
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"
)
if (findNavController().currentDestination?.id == R.id.landingFragment) {
val action =
LandingFragmentDirections.actionLandingFragmentToRecoverAccountFragment()
findNavController().navigate(action)
}
}

View file

@ -0,0 +1,106 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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.RecoverPhoneAccountViewModel
import kotlin.getValue
@UiThread
class RecoverAccountFragment : GenericFragment() {
companion object {
private const val TAG = "[Recover Account Fragment]"
}
private lateinit var binding: AssistantRecoverAccountFragmentBinding
private val viewModel: RecoverPhoneAccountViewModel 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)
binding.setBackClickListener {
goBack()
}
binding.setRecoverEmailAccountClickListener {
recoverEmailAccount()
}
binding.setRecoverPhoneNumberAccountClickListener {
if (findNavController().currentDestination?.id == R.id.recoverAccountFragment) {
val action = RecoverAccountFragmentDirections.actionRecoverAccountFragmentToRecoverPhoneAccountFragment()
findNavController().navigate(action)
}
}
}
private fun goBack() {
findNavController().popBackStack()
}
private fun recoverEmailAccount() {
val url = getString(R.string.web_platform_forgotten_password_url)
try {
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"
)
}
}
}

View file

@ -0,0 +1,100 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.assistant.fragment
import android.content.ClipboardManager
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.UiThread
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantRecoverPhoneAccountConfirmSmsCodeFragmentBinding
import org.linphone.ui.GenericFragment
import org.linphone.ui.assistant.viewmodel.RecoverPhoneAccountViewModel
@UiThread
class RecoverPhoneAccountCodeConfirmationFragment : GenericFragment() {
companion object {
private const val TAG = "[Recover Phone Account Code Confirmation Fragment]"
}
private lateinit var binding: AssistantRecoverPhoneAccountConfirmSmsCodeFragmentBinding
private val viewModel: RecoverPhoneAccountViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantRecoverPhoneAccountConfirmSmsCodeFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
goBack()
}
viewModel.accountCreatedEvent.observe(viewLifecycleOwner) {
it.consume { identity ->
Log.i("$TAG Account [$identity] has been created, leaving assistant")
requireActivity().finish()
}
}
// This won't work starting Android 10 as clipboard access is denied unless app has focus,
// which won't be the case when the SMS arrives unless it is added into clipboard from a notification
val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
clipboard.addPrimaryClipChangedListener {
val data = clipboard.primaryClip
if (data != null && data.itemCount > 0) {
val clip = data.getItemAt(0).text.toString()
if (clip.length == 4) {
Log.i(
"$TAG Found 4 digits [$clip] as primary clip in clipboard, using it and clear it"
)
viewModel.smsCodeFirstDigit.value = clip[0].toString()
viewModel.smsCodeSecondDigit.value = clip[1].toString()
viewModel.smsCodeThirdDigit.value = clip[2].toString()
viewModel.smsCodeLastDigit.value = clip[3].toString()
clipboard.clearPrimaryClip()
}
}
}
}
private fun goBack() {
findNavController().popBackStack()
}
}

View file

@ -0,0 +1,183 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.assistant.fragment
import android.content.Context
import android.os.Bundle
import android.telephony.TelephonyManager
import android.text.Editable
import android.text.TextWatcher
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.appcompat.widget.AppCompatTextView
import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.ui.GenericFragment
import org.linphone.databinding.AssistantRecoverPhoneAccountFragmentBinding
import org.linphone.ui.assistant.viewmodel.RecoverPhoneAccountViewModel
import org.linphone.utils.AppUtils
import org.linphone.utils.ConfirmationDialogModel
import org.linphone.utils.DialogUtils
import org.linphone.utils.PhoneNumberUtils
import kotlin.getValue
@UiThread
class RecoverPhoneAccountFragment : GenericFragment() {
companion object {
private const val TAG = "[Recover Phone Account Fragment]"
}
private lateinit var binding: AssistantRecoverPhoneAccountFragmentBinding
private val viewModel: RecoverPhoneAccountViewModel by navGraphViewModels(
R.id.assistant_nav_graph
)
private val dropdownListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val dialPlan = viewModel.dialPlansList[position]
Log.i(
"$TAG Selected dialplan updated [+${dialPlan.countryCallingCode}] / [${dialPlan.country}]"
)
viewModel.selectedDialPlan.value = dialPlan
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantRecoverPhoneAccountFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
observeToastEvents(viewModel)
binding.setBackClickListener {
goBack()
}
binding.phoneNumber.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
viewModel.phoneNumberError.value = ""
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
viewModel.normalizedPhoneNumberEvent.observe(viewLifecycleOwner) {
it.consume { number ->
Log.i("$TAG Showing confirmation dialog for phone number [$number]")
showPhoneNumberConfirmationDialog(number)
}
}
viewModel.goToSmsValidationEvent.observe(viewLifecycleOwner) {
it.consume {
if (findNavController().currentDestination?.id == R.id.recoverPhoneAccountFragment) {
Log.i("$TAG Going to SMS code validation fragment")
val action = RecoverPhoneAccountFragmentDirections.actionRecoverPhoneAccountFragmentToRecoverPhoneAccountCodeConfirmationFragment()
findNavController().navigate(action)
}
}
}
val telephonyManager = requireContext().getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val countryIso = telephonyManager.networkCountryIso
coreContext.postOnCoreThread {
val fragmentContext = context ?: return@postOnCoreThread
val adapter = object : ArrayAdapter<String>(
fragmentContext,
R.layout.drop_down_item,
viewModel.dialPlansLabelList
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = convertView ?: super.getView(position, null, parent)
val label = viewModel.dialPlansShortLabelList[position]
(view as? AppCompatTextView)?.text = label
return view
}
}
adapter.setDropDownViewResource(R.layout.assistant_country_picker_dropdown_cell)
val dialPlan = PhoneNumberUtils.getDeviceDialPlan(countryIso)
var default = 0
if (dialPlan != null) {
viewModel.selectedDialPlan.postValue(dialPlan)
default = viewModel.dialPlansList.indexOf(dialPlan)
}
coreContext.postOnMainThread {
binding.prefix.adapter = adapter
binding.prefix.setSelection(default)
binding.prefix.onItemSelectedListener = dropdownListener
}
}
}
private fun goBack() {
findNavController().popBackStack()
}
private fun showPhoneNumberConfirmationDialog(number: String) {
val label = AppUtils.getFormattedString(R.string.assistant_dialog_confirm_phone_number_message, number)
val model = ConfirmationDialogModel(label)
val dialog = DialogUtils.getAccountCreationPhoneNumberConfirmationDialog(
requireActivity(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
Log.w("$TAG User dismissed the dialog, aborting recovery process")
dialog.dismiss()
}
}
model.confirmEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG User confirmed the phone number, requesting account creation token & SMS code")
viewModel.startRecoveryProcess()
dialog.dismiss()
}
}
dialog.show()
}
}

View file

@ -365,7 +365,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(

View file

@ -0,0 +1,449 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
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.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.AccountCreator
import org.linphone.core.AccountCreatorListenerStub
import org.linphone.core.AccountManagerServices
import org.linphone.core.AccountManagerServicesRequest
import org.linphone.core.AccountManagerServicesRequestListenerStub
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.DialPlan
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.Event
import org.linphone.utils.LinphoneUtils
import java.util.Locale
class RecoverPhoneAccountViewModel : GenericViewModel() {
companion object {
private const val TAG = "[Recover Phone Account ViewModel]"
private const val TIME_TO_WAIT_FOR_PUSH_NOTIFICATION_WITH_ACCOUNT_CREATION_TOKEN = 5000
}
val pushNotificationsAvailable = MutableLiveData<Boolean>()
val dialPlansLabelList = arrayListOf<String>()
val dialPlansShortLabelList = arrayListOf<String>()
val dialPlansList = arrayListOf<DialPlan>()
val selectedDialPlan = MutableLiveData<DialPlan>()
val phoneNumber = MutableLiveData<String>()
val phoneNumberError = MutableLiveData<String>()
val confirmationMessage = MutableLiveData<String>()
val smsCodeFirstDigit = MutableLiveData<String>()
val smsCodeSecondDigit = MutableLiveData<String>()
val smsCodeThirdDigit = MutableLiveData<String>()
val smsCodeLastDigit = MutableLiveData<String>()
val operationInProgress = MutableLiveData<Boolean>()
val recoverEnabled = MediatorLiveData<Boolean>()
private var normalizedPhoneNumber: String? = null
val normalizedPhoneNumberEvent = MutableLiveData<Event<String>>()
val goToSmsValidationEvent = MutableLiveData<Event<Boolean>>()
val accountCreatedEvent = MutableLiveData<Event<String>>()
private lateinit var accountManagerServices: AccountManagerServices
private val accountManagerServicesListener = object : AccountManagerServicesRequestListenerStub() {
@WorkerThread
override fun onRequestSuccessful(
request: AccountManagerServicesRequest,
data: String?
) {
Log.i("$TAG Request [$request] was successful, data is [$data]")
operationInProgress.postValue(false)
}
@WorkerThread
override fun onRequestError(
request: AccountManagerServicesRequest,
statusCode: Int,
errorMessage: String?,
parameterErrors: Dictionary?
) {
Log.e(
"$TAG Request [$request] returned an error with status code [$statusCode] and message [$errorMessage]"
)
operationInProgress.postValue(false)
if (!errorMessage.isNullOrEmpty()) {
showFormattedRedToast(errorMessage, R.drawable.warning_circle)
}
when (request.type) {
AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush -> {
Log.w("$TAG Cancelling job waiting for push notification")
waitingForFlexiApiPushToken = false
waitForPushJob?.cancel()
}
else -> {
}
}
recoverEnabled.postValue(true)
}
}
private var accountCreationToken: String? = null
private var waitingForFlexiApiPushToken = false
private var waitForPushJob: Job? = null
private lateinit var accountCreator: AccountCreator
private val accountCreatorListener = object : AccountCreatorListenerStub() {
@WorkerThread
override fun onRecoverAccount(
creator: AccountCreator,
status: AccountCreator.Status,
response: String?
) {
Log.i("$TAG Recover account status is $status")
operationInProgress.postValue(false)
if (status == AccountCreator.Status.RequestOk) {
goToSmsValidationEvent.postValue(Event(true))
} else {
Log.e("$TAG Error in onRecoverAccount [${status.name}]")
showFormattedRedToast(status.name, R.drawable.warning_circle)
}
}
@WorkerThread
override fun onLoginLinphoneAccount(
creator: AccountCreator,
status: AccountCreator.Status,
response: String?
) {
Log.i("$TAG onLoginLinphoneAccount status is $status")
operationInProgress.postValue(false)
if (status == AccountCreator.Status.RequestOk) {
if (!createAccountAndAuthInfo()) {
Log.e("$TAG Failed to create account object")
}
} else {
Log.e("$TAG Error in onRecoverAccount [${status.name}]")
showFormattedRedToast(status.name, R.drawable.warning_circle)
}
}
}
private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onPushNotificationReceived(core: Core, payload: String?) {
Log.i("$TAG Push received: [$payload]")
val data = payload.orEmpty()
if (data.isNotEmpty()) {
try {
// This is because JSONObject.toString() done by the SDK will result in payload looking like {"custom-payload":"{\"token\":\"value\"}"}
val cleanPayload = data
.replace("\\\"", "\"")
.replace("\"{", "{")
.replace("}\"", "}")
Log.i("$TAG Cleaned payload is: [$cleanPayload]")
val json = JSONObject(cleanPayload)
val customPayload = json.getJSONObject("custom-payload")
if (customPayload.has("token")) {
waitForPushJob?.cancel()
waitingForFlexiApiPushToken = false
operationInProgress.postValue(false)
val token = customPayload.getString("token")
if (token.isNotEmpty()) {
accountCreationToken = token
Log.i(
"$TAG Extracted token [$accountCreationToken] from push payload, recovering account"
)
requestSmsCode()
} else {
Log.e("$TAG Push payload JSON object has an empty 'token'!")
onFlexiApiTokenRequestError()
}
} else {
Log.e("$TAG Push payload JSON object has no 'token' key!")
onFlexiApiTokenRequestError()
}
} catch (e: JSONException) {
Log.e("$TAG Exception trying to parse push payload as JSON: [$e]")
onFlexiApiTokenRequestError()
}
} else {
Log.e("$TAG Push payload is null or empty, can't extract auth token!")
onFlexiApiTokenRequestError()
}
}
}
init {
coreContext.postOnCoreThread { core ->
core.addListener(coreListener)
pushNotificationsAvailable.postValue(LinphoneUtils.arePushNotificationsAvailable(core))
val dialPlans = Factory.instance().dialPlans.toList()
for (dialPlan in dialPlans) {
dialPlansList.add(dialPlan)
dialPlansLabelList.add(
"${dialPlan.flag} ${dialPlan.country} | +${dialPlan.countryCallingCode}"
)
dialPlansShortLabelList.add(
"${dialPlan.flag} +${dialPlan.countryCallingCode}"
)
}
accountManagerServices = core.createAccountManagerServices()
accountManagerServices.language = Locale.getDefault().language // Returns en, fr, etc...
accountCreator = core.createAccountCreator("https://subscribe.linphone.org/api/")
accountCreator.addListener(accountCreatorListener)
}
recoverEnabled.addSource(selectedDialPlan) {
recoverEnabled.value = phoneNumber.value.orEmpty().isNotEmpty() && selectedDialPlan.value?.countryCallingCode.orEmpty().isNotEmpty()
}
recoverEnabled.addSource(phoneNumber) {
recoverEnabled.value = phoneNumber.value.orEmpty().isNotEmpty() && selectedDialPlan.value?.countryCallingCode.orEmpty().isNotEmpty()
}
}
override fun onCleared() {
coreContext.postOnCoreThread { core ->
core.removeListener(coreListener)
accountCreator.removeListener(accountCreatorListener)
}
super.onCleared()
}
@UiThread
fun sendCode() {
coreContext.postOnCoreThread {
val dialPlan = selectedDialPlan.value
if (dialPlan == null) {
Log.e("$TAG No dial plan (country) selected!")
return@postOnCoreThread
}
val number = phoneNumber.value.orEmpty().trim()
val formattedPhoneNumber = dialPlan.formatPhoneNumber(number, false)
Log.i(
"$TAG Formatted phone number [$number] using dial plan [${dialPlan.country}] is [$formattedPhoneNumber]"
)
val message = coreContext.context.getString(
R.string.assistant_account_creation_sms_confirmation_explanation,
formattedPhoneNumber
)
normalizedPhoneNumber = formattedPhoneNumber
confirmationMessage.postValue(message)
normalizedPhoneNumberEvent.postValue(Event(formattedPhoneNumber))
}
}
@WorkerThread
fun requestSmsCode() {
operationInProgress.postValue(true)
coreContext.core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath)
accountCreator.domain = corePreferences.defaultDomain
val dialPlan = selectedDialPlan.value
if (dialPlan == null) {
Log.e("$TAG No dial plan (country) selected!")
return
}
val number = phoneNumber.value.orEmpty().trim()
val countryCallingCode = dialPlan.countryCallingCode
var result = AccountCreator.PhoneNumberStatus.fromInt(
accountCreator.setPhoneNumber(number, countryCallingCode)
)
if (result != AccountCreator.PhoneNumberStatus.Ok) {
Log.e(
"$TAG Error [$result] setting the phone number: $number with prefix: $countryCallingCode"
)
phoneNumberError.postValue(result.name)
operationInProgress.postValue(false)
return
}
Log.i("$TAG Phone number is ${accountCreator.phoneNumber}")
val result2 = accountCreator.setUsername(accountCreator.phoneNumber)
if (result2 != AccountCreator.UsernameStatus.Ok) {
Log.e(
"$TAG Error [${result2.name}] setting the username: ${accountCreator.phoneNumber}"
)
phoneNumberError.postValue(result2.name)
operationInProgress.postValue(false)
return
}
Log.i("$TAG Username is ${accountCreator.username}")
accountCreator.token = accountCreationToken
Log.i("$TAG Token is ${accountCreator.token}")
val status = accountCreator.recoverAccount()
Log.i("$TAG Recover account returned $status")
if (status != AccountCreator.Status.RequestOk) {
operationInProgress.postValue(false)
Log.e("$TAG Error doing recoverAccount [${status.name}]")
showFormattedRedToast(status.name, R.drawable.warning_circle)
}
}
@UiThread
fun validateCode() {
operationInProgress.value = true
coreContext.postOnCoreThread { core ->
val code =
"${smsCodeFirstDigit.value.orEmpty().trim()}${smsCodeSecondDigit.value.orEmpty().trim()}${smsCodeThirdDigit.value.orEmpty().trim()}${smsCodeLastDigit.value.orEmpty().trim()}"
accountCreator.activationCode = code
val status = accountCreator.loginLinphoneAccount()
Log.i("$TAG Code [$code] validation result is $status")
if (status != AccountCreator.Status.RequestOk) {
operationInProgress.postValue(false)
Log.e("$TAG Error doing loginLinphoneAccount [${status.name}]")
showFormattedRedToast(status.name, R.drawable.warning_circle)
}
// Reset code
smsCodeFirstDigit.postValue("")
smsCodeSecondDigit.postValue("")
smsCodeThirdDigit.postValue("")
smsCodeLastDigit.postValue("")
}
}
@WorkerThread
private fun createAccountAndAuthInfo(): Boolean {
val account = accountCreator.createAccountInCore()
if (account == null) {
Log.e("$TAG Account creator couldn't create account")
return false
}
coreContext.core.defaultAccount = account
val username = account.params.identityAddress?.username.orEmpty()
Log.i("$TAG Account created with username [$username]")
accountCreatedEvent.postValue(Event(username))
return true
}
@UiThread
fun startRecoveryProcess() {
coreContext.postOnCoreThread {
requestFlexiApiToken()
}
}
@WorkerThread
private fun requestFlexiApiToken() {
if (!coreContext.core.isPushNotificationAvailable) {
Log.e(
"$TAG Core says push notification aren't available, can't request a token from FlexiAPI"
)
onFlexiApiTokenRequestError()
return
}
operationInProgress.postValue(true)
recoverEnabled.postValue(false)
val pushConfig = coreContext.core.pushNotificationConfig
if (pushConfig != null) {
val provider = pushConfig.provider
val param = pushConfig.param
val prid = pushConfig.prid
if (provider.isNullOrEmpty() || param.isNullOrEmpty() || prid.isNullOrEmpty()) {
Log.e(
"$TAG At least one mandatory push information (provider [$provider], param [$param], prid [$prid]) is missing!"
)
onFlexiApiTokenRequestError()
return
}
// Request an auth token, will be sent by push
val request = accountManagerServices.createSendAccountCreationTokenByPushRequest(
provider,
param,
prid
)
request.addListener(accountManagerServicesListener)
request.submit()
val waitFor = TIME_TO_WAIT_FOR_PUSH_NOTIFICATION_WITH_ACCOUNT_CREATION_TOKEN
waitingForFlexiApiPushToken = true
waitForPushJob?.cancel()
Log.i("$TAG Waiting push with auth token for $waitFor ms")
waitForPushJob = viewModelScope.launch {
withContext(Dispatchers.IO) {
delay(waitFor.toLong())
}
withContext(Dispatchers.Main) {
if (waitingForFlexiApiPushToken) {
waitingForFlexiApiPushToken = false
Log.e("$TAG Auth token wasn't received by push in [$waitFor] ms")
onFlexiApiTokenRequestError()
}
}
}
} else {
Log.e("$TAG No push configuration object in Core, shouldn't happen!")
onFlexiApiTokenRequestError()
}
}
@WorkerThread
private fun onFlexiApiTokenRequestError() {
Log.e("$TAG Flexi API token request by push error!")
operationInProgress.postValue(false)
showRedToast(R.string.assistant_account_register_push_notification_not_received_error, R.drawable.warning_circle)
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="256"
android:viewportHeight="256">
<path
android:pathData="M48,56L48,200a8,8 0,0 1,-16 0L32,56a8,8 0,0 1,16 0ZM140,110.5L120,117L120,96a8,8 0,0 0,-16 0v21L84,110.5a8,8 0,0 0,-5 15.22l20,6.49 -12.34,17a8,8 0,1 0,12.94 9.4l12.34,-17 12.34,17a8,8 0,1 0,12.94 -9.4l-12.34,-17 20,-6.49A8,8 0,0 0,140 110.5ZM246,115.64A8,8 0,0 0,236 110.5L216,117L216,96a8,8 0,0 0,-16 0v21l-20,-6.49a8,8 0,0 0,-4.95 15.22l20,6.49 -12.34,17a8,8 0,1 0,12.94 9.4l12.34,-17 12.34,17a8,8 0,1 0,12.94 -9.4l-12.34,-17 20,-6.49A8,8 0,0 0,246 115.64Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -214,7 +214,7 @@
android:onClick="@{qrCodeClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
@ -235,7 +235,7 @@
android:onClick="@{thirdPartySipAccountLoginClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"

View file

@ -0,0 +1,181 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="android.text.InputType" />
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="recoverEmailAccountClickListener"
type="View.OnClickListener" />
<variable
name="recoverPhoneNumberAccountClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.assistant.viewmodel.RecoverPhoneAccountViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/color_main2_000">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:onClick="@{backClickListener}"
android:id="@+id/back"
android:layout_width="@dimen/top_bar_height"
android:layout_height="@dimen/top_bar_height"
android:padding="15dp"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main2_500"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/assistant_page_title_style"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/assistant_forgotten_password_title"
android:textColor="?attr/color_text"
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/password_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="68dp"
android:background="@drawable/circle_light_blue_button_background"
android:padding="16dp"
android:src="@drawable/password"
android:contentDescription="@null"
app:tint="?attr/color_main2_500"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/header_style"
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="48dp"
android:layout_marginEnd="48dp"
android:textAlignment="center"
android:text="@string/assistant_forgotten_password_subtitle"
app:layout_constraintTop_toBottomOf="@id/password_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="48dp"
android:layout_marginEnd="48dp"
android:text="@string/assistant_forgotten_password_message"
android:textSize="14sp"
android:textColor="?attr/color_main2_600"
android:gravity="center_horizontal"
app:layout_constraintTop_toBottomOf="@id/subtitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/primary_button_label_style"
android:id="@+id/recover_email"
android:onClick="@{recoverEmailAccountClickListener}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@string/assistant_recover_email_account_label"
app:layout_constraintWidth_max="@dimen/button_max_width"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/message" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/primary_button_label_style"
android:id="@+id/recover_phone_number"
android:onClick="@{recoverPhoneNumberAccountClickListener}"
android:enabled="@{viewModel.pushNotificationsAvailable}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@string/assistant_recover_phone_number_account_label"
app:layout_constraintWidth_max="@dimen/button_max_width"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/recover_email" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/header_style"
android:id="@+id/no_push_label"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_marginStart="48dp"
android:layout_marginEnd="48dp"
android:textAlignment="center"
android:visibility="@{viewModel.pushNotificationsAvailable ? View.GONE : View.VISIBLE, default=gone}"
android:text="@string/assistant_recover_phone_number_account_unavailable_no_push_warning"
app:layout_constraintTop_toBottomOf="@id/recover_phone_number"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/mountains"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:src="@drawable/mountains"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:contentDescription="@null"
app:layout_constraintVertical_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/no_push_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:tint="?attr/color_main1_500" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.operationInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="android.text.InputType" />
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.assistant.viewmodel.RecoverPhoneAccountViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:onClick="@{backClickListener}"
android:id="@+id/back"
android:layout_width="@dimen/top_bar_height"
android:layout_height="@dimen/top_bar_height"
android:padding="15dp"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main2_500"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/assistant_page_title_style"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/assistant_account_creation_sms_confirmation_title"
android:textColor="?attr/color_text"
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:text="@{viewModel.confirmationMessage, default=@string/assistant_account_creation_sms_confirmation_explanation}"
android:textSize="14sp"
android:textColor="?attr/color_main2_600"
android:gravity="center_horizontal"
app:layout_constraintTop_toBottomOf="@id/title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/illu"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="100dp"
android:adjustViewBounds="true"
android:src="@drawable/confirm_sms_code_illu"
android:contentDescription="@null"
app:layout_constraintTop_toBottomOf="@id/wrong_number"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatEditText
focusNextOnInput="@{true}"
style="@style/default_text_style_300"
android:id="@+id/code_first_digit"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:background="@drawable/shape_assistant_sms_code_confirmation"
android:text="@={viewModel.smsCodeFirstDigit, default=`1`}"
android:maxLength="1"
android:textColor="@color/assistant_sms_confirmation_code_color"
android:textSize="50sp"
android:textCursorDrawable="@color/transparent_color"
android:gravity="center"
android:inputType="number"
android:imeOptions="actionNext"
android:nextFocusDown="@id/code_second_digit"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/code_second_digit" />
<androidx.appcompat.widget.AppCompatEditText
focusNextOnInput="@{true}"
style="@style/default_text_style_300"
android:id="@+id/code_second_digit"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:background="@drawable/shape_assistant_sms_code_confirmation"
android:text="@={viewModel.smsCodeSecondDigit, default=`2`}"
android:maxLength="1"
android:textColor="@color/assistant_sms_confirmation_code_color"
android:textSize="50sp"
android:textCursorDrawable="@color/transparent_color"
android:gravity="center"
android:inputType="number"
android:imeOptions="actionNext"
android:nextFocusDown="@id/code_third_digit"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintStart_toEndOf="@id/code_first_digit"
app:layout_constraintEnd_toStartOf="@id/code_third_digit" />
<androidx.appcompat.widget.AppCompatEditText
focusNextOnInput="@{true}"
style="@style/default_text_style_300"
android:id="@+id/code_third_digit"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:background="@drawable/shape_assistant_sms_code_confirmation"
android:text="@={viewModel.smsCodeThirdDigit, default=`3`}"
android:maxLength="1"
android:textColor="@color/assistant_sms_confirmation_code_color"
android:textSize="50sp"
android:textCursorDrawable="@color/transparent_color"
android:gravity="center"
android:inputType="number"
android:imeOptions="actionNext"
android:nextFocusDown="@id/code_last_digit"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintStart_toEndOf="@id/code_second_digit"
app:layout_constraintEnd_toStartOf="@id/code_last_digit" />
<androidx.appcompat.widget.AppCompatEditText
validateOnInput="@{() -> viewModel.validateCode()}"
style="@style/default_text_style_300"
android:id="@+id/code_last_digit"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginTop="32dp"
android:background="@drawable/shape_assistant_sms_code_confirmation"
android:text="@={viewModel.smsCodeLastDigit, default=`4`}"
android:maxLength="1"
android:textColor="@color/assistant_sms_confirmation_code_color"
android:textSize="50sp"
android:textCursorDrawable="@color/transparent_color"
android:gravity="center"
android:inputType="number"
android:imeOptions="actionDone"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintStart_toEndOf="@id/code_third_digit"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_600"
android:id="@+id/wrong_number"
android:onClick="@{backClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="51dp"
android:paddingTop="6dp"
android:paddingBottom="6dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/assistant_account_creation_wrong_phone_number"
android:textSize="13sp"
android:textColor="@color/secondary_button_label_color"
android:gravity="center"
android:background="@drawable/secondary_button_background"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/code_first_digit" />
<ImageView
android:id="@+id/mountains"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:src="@drawable/mountains"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:contentDescription="@null"
app:layout_constraintVertical_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/wrong_number"
app:layout_constraintBottom_toBottomOf="parent"
app:tint="?attr/color_main1_500" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.operationInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -0,0 +1,212 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/tools">
<data>
<import type="android.view.View" />
<import type="android.text.InputType" />
<variable
name="backClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.assistant.viewmodel.RecoverPhoneAccountViewModel" />
</data>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/color_main2_000">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:onClick="@{backClickListener}"
android:id="@+id/back"
android:layout_width="@dimen/top_bar_height"
android:layout_height="@dimen/top_bar_height"
android:padding="15dp"
android:src="@drawable/caret_left"
android:contentDescription="@string/content_description_go_back_icon"
app:tint="?attr/color_main2_500"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/assistant_page_title_style"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/assistant_forgotten_password_title"
android:textColor="?attr/color_text"
app:layout_constraintTop_toTopOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:id="@+id/password_icon"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_marginTop="68dp"
android:background="@drawable/circle_light_blue_button_background"
android:padding="16dp"
android:src="@drawable/password"
android:contentDescription="@null"
app:tint="?attr/color_main2_500"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/header_style"
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="48dp"
android:layout_marginEnd="48dp"
android:textAlignment="center"
android:text="@string/assistant_recover_phone_number_account_subtitle"
app:layout_constraintTop_toBottomOf="@id/password_icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/header_style"
android:id="@+id/phone_number_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="@{@string/phone_number + `*`, default=`Phone number*`}"
app:layout_constraintTop_toBottomOf="@id/subtitle"
app:layout_constraintStart_toStartOf="@id/prefix"/>
<ImageView
android:id="@+id/phone_number_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@{viewModel.phoneNumberError.length() > 0 ? @drawable/shape_edit_text_error_background : @drawable/edit_text_background, default=@drawable/edit_text_background}"
android:contentDescription="@null"
app:layout_constraintStart_toStartOf="@id/prefix"
app:layout_constraintEnd_toEndOf="@id/phone_number"
app:layout_constraintTop_toTopOf="@id/prefix"
app:layout_constraintBottom_toBottomOf="@id/prefix" />
<androidx.appcompat.widget.AppCompatSpinner
style="@style/default_text_style"
android:id="@+id/prefix"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:paddingStart="20dp"
android:paddingEnd="25dp"
android:textSize="14sp"
android:textColor="?attr/color_main2_600"
android:gravity="center_vertical"
android:overlapAnchor="false"
android:dropDownVerticalOffset="25dp"
android:spinnerMode="dropdown"
android:popupBackground="@drawable/shape_squircle_white_background"
android:background="@color/transparent_color"
app:layout_constraintTop_toTopOf="@id/phone_number"
app:layout_constraintBottom_toBottomOf="@id/phone_number"
app:layout_constraintStart_toStartOf="@id/send_code" />
<ImageView
android:id="@+id/prefix_caret"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/caret_down"
android:contentDescription="@null"
app:tint="?attr/color_main2_600"
app:layout_constraintTop_toTopOf="@id/prefix"
app:layout_constraintBottom_toBottomOf="@id/prefix"
app:layout_constraintEnd_toEndOf="@id/prefix"/>
<androidx.appcompat.widget.AppCompatEditText
style="@style/default_text_style"
android:id="@+id/phone_number"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginStart="5dp"
android:paddingEnd="20dp"
android:text="@={viewModel.phoneNumber, default=`6 01 02 03 04 05`}"
android:textSize="14sp"
android:textColor="?attr/color_main2_600"
android:inputType="phone"
android:drawableStart="@drawable/separator"
android:drawablePadding="10dp"
android:hint="@string/phone_number"
android:background="@color/transparent_color"
app:layout_constraintWidth_max="@dimen/text_input_max_width"
app:layout_constraintTop_toBottomOf="@id/phone_number_label"
app:layout_constraintStart_toEndOf="@id/prefix_caret"
app:layout_constraintEnd_toEndOf="@id/send_code"
bind:ignore="RtlSymmetry" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_600"
android:visibility="@{viewModel.phoneNumberError.length() == 0 ? View.GONE : View.VISIBLE, default=gone}"
android:id="@+id/phone_number_error"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.phoneNumberError, default=`Error`}"
android:textSize="13sp"
android:textColor="?attr/color_danger_500"
app:layout_constraintTop_toBottomOf="@id/prefix"
app:layout_constraintStart_toStartOf="@id/prefix"
app:layout_constraintEnd_toEndOf="@id/phone_number"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/primary_button_label_style"
android:id="@+id/send_code"
android:onClick="@{() -> viewModel.sendCode()}"
android:enabled="@{viewModel.recoverEnabled &amp;&amp; !viewModel.operationInProgress, default=false}"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="@string/assistant_recover_phone_number_account_send_code_label"
app:layout_constraintWidth_max="@dimen/button_max_width"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/phone_number_error" />
<ImageView
android:id="@+id/mountains"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:src="@drawable/mountains"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
android:contentDescription="@null"
app:layout_constraintVertical_bias="1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/send_code"
app:layout_constraintBottom_toBottomOf="parent"
app:tint="?attr/color_main1_500" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
<include
layout="@layout/operation_in_progress"
bind:visibility="@{viewModel.operationInProgress}" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

View file

@ -144,6 +144,14 @@
app:launchSingleTop="true"
app:popUpTo="@id/landingFragment"
app:popUpToInclusive="true"/>
<action
android:id="@+id/action_landingFragment_to_recoverAccountFragment"
app:destination="@id/recoverAccountFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true"/>
</fragment>
@ -169,4 +177,40 @@
app:enterAnim="@anim/slide_in"
app:popExitAnim="@anim/slide_out" />
<fragment
android:id="@+id/recoverAccountFragment"
android:name="org.linphone.ui.assistant.fragment.RecoverAccountFragment"
android:label="RecoverAccountFragment"
tools:layout="@layout/assistant_recover_account_fragment">
<action
android:id="@+id/action_recoverAccountFragment_to_recoverPhoneAccountFragment"
app:destination="@id/recoverPhoneAccountFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true"/>
</fragment>
<fragment
android:id="@+id/recoverPhoneAccountFragment"
android:name="org.linphone.ui.assistant.fragment.RecoverPhoneAccountFragment"
android:label="RecoverPhoneAccountFragment"
tools:layout="@layout/assistant_recover_phone_account_fragment">
<action
android:id="@+id/action_recoverPhoneAccountFragment_to_recoverPhoneAccountCodeConfirmationFragment"
app:destination="@id/recoverPhoneAccountCodeConfirmationFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right"
app:launchSingleTop="true"/>
</fragment>
<fragment
android:id="@+id/recoverPhoneAccountCodeConfirmationFragment"
android:name="org.linphone.ui.assistant.fragment.RecoverPhoneAccountCodeConfirmationFragment"
android:label="RecoverPhoneAccountCodeConfirmationFragment"
tools:layout="@layout/assistant_recover_phone_account_confirm_sms_code_fragment"/>
</navigation>

View file

@ -133,6 +133,14 @@
<string name="assistant_permissions_post_notifications_title"><b>Notifications :</b> Pour vous informer quand vous recevez un message ou un appel.</string>
<string name="assistant_permissions_record_audio_title"><b>Microphone :</b> Pour permettre à vos correspondants de vous entendre. </string>
<string name="assistant_permissions_access_camera_title"><b>Caméra :</b> Pour capturer votre vidéo lors des appels et des conférences.</string>
<string name="assistant_forgotten_password_title">Mot de passe oublié</string>
<string name="assistant_forgotten_password_subtitle">Choisissez comment récupérer votre compte.</string>
<string name="assistant_forgotten_password_message">Vous avez créé votre compte avec :</string>
<string name="assistant_recover_email_account_label">Un email</string>
<string name="assistant_recover_phone_number_account_label">Un numéro de téléphone</string>
<string name="assistant_recover_phone_number_account_subtitle">Veuillez saisir le numéro de téléphone lié à votre compte, nous allons vous envoyer un code.</string>
<string name="assistant_recover_phone_number_account_send_code_label">Envoyer le code</string>
<string name="assistant_recover_phone_number_account_unavailable_no_push_warning">Les notifications push ne semblent pas être disponibles sur votre appareil. Celles-ci sont nécessaires à la récupération dun compte sur lapplication mobile avec un numéro de téléphone.</string>
<!-- Main navigation items -->
<string name="bottom_navigation_contacts_label">Contacts</string>

View file

@ -27,7 +27,7 @@
<string name="emoji_tear" translatable="false">😢</string>
<string name="help_about_open_source_licenses_title" translatable="false">GNU General Public License v3.0</string>
<string name="help_about_open_source_licenses_subtitle" translatable="false">© Belledonne Communications 2010-2024</string>
<string name="help_about_open_source_licenses_subtitle" translatable="false">© Belledonne Communications 2010-2025</string>
<string name="help_advanced_send_debug_logs_email_address" translatable="false">linphone-android@belledonne-communications.com</string>
<string name="website_contact_url" translatable="false">https://linphone.org/contact</string>
@ -173,6 +173,14 @@
<string name="assistant_permissions_post_notifications_title"><b>Post notifications:</b> To be informed when you receive a message or a call.</string>
<string name="assistant_permissions_record_audio_title"><b>Record audio:</b> So your correspondent can hear you and to record voice messages.</string>
<string name="assistant_permissions_access_camera_title"><b>Access camera:</b> To capture video during video calls and conferences.</string>
<string name="assistant_forgotten_password_title">Password forgotten</string>
<string name="assistant_forgotten_password_subtitle">Choose how to recover your account.</string>
<string name="assistant_forgotten_password_message">You created your account using:</string>
<string name="assistant_recover_email_account_label">An email</string>
<string name="assistant_recover_phone_number_account_label">A phone number</string>
<string name="assistant_recover_phone_number_account_subtitle">Enter the phone number linked to your account, we\'ll send you a code.</string>
<string name="assistant_recover_phone_number_account_send_code_label">Send the code</string>
<string name="assistant_recover_phone_number_account_unavailable_no_push_warning">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.</string>
<!-- Main navigation items -->
<string name="bottom_navigation_contacts_label">Contacts</string>