Started account creation

This commit is contained in:
Sylvain Berfini 2023-09-04 14:22:09 +02:00
parent 489483e22a
commit 2fe1bcbdff
20 changed files with 1220 additions and 25 deletions

View file

@ -13,7 +13,7 @@
<!-- Needed for full screen intent in incoming call notifications --> <!-- Needed for full screen intent in incoming call notifications -->
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" /> <uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<!-- To vibrate when pressing DTMF keys on numpad & incoming calls --> <!-- To vibrate while receiving an incoming call -->
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<!-- Needed for foreground service <!-- Needed for foreground service

View file

@ -19,6 +19,8 @@
*/ */
package org.linphone.ui.assistant.fragment package org.linphone.ui.assistant.fragment
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -27,10 +29,13 @@ import androidx.annotation.UiThread
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.navGraphViewModels import androidx.navigation.navGraphViewModels
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R import org.linphone.R
import org.linphone.core.tools.Log
import org.linphone.databinding.AssistantLoginFragmentBinding import org.linphone.databinding.AssistantLoginFragmentBinding
import org.linphone.ui.assistant.viewmodel.AssistantViewModel import org.linphone.ui.assistant.viewmodel.AccountLoginViewModel
import org.linphone.ui.main.fragment.GenericFragment import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.PhoneNumberUtils
@UiThread @UiThread
class LoginFragment : GenericFragment() { class LoginFragment : GenericFragment() {
@ -40,7 +45,7 @@ class LoginFragment : GenericFragment() {
private lateinit var binding: AssistantLoginFragmentBinding private lateinit var binding: AssistantLoginFragmentBinding
private val viewModel: AssistantViewModel by navGraphViewModels( private val viewModel: AccountLoginViewModel by navGraphViewModels(
R.id.loginFragment R.id.loginFragment
) )
@ -72,6 +77,16 @@ class LoginFragment : GenericFragment() {
findNavController().navigate(action) findNavController().navigate(action)
} }
binding.setForgottenPasswordClickListener {
try {
val url = "https://subscribe.linphone.org"
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(browserIntent)
} catch (ise: IllegalStateException) {
Log.e("$TAG Can't start ACTION_VIEW intent, IllegalStateException: $ise")
}
}
binding.setQrCodeClickListener { binding.setQrCodeClickListener {
val action = LoginFragmentDirections.actionLoginFragmentToQrCodeScannerFragment() val action = LoginFragmentDirections.actionLoginFragmentToQrCodeScannerFragment()
findNavController().navigate(action) findNavController().navigate(action)
@ -87,6 +102,11 @@ class LoginFragment : GenericFragment() {
goBack() goBack()
} }
} }
coreContext.postOnCoreThread {
val prefix = PhoneNumberUtils.getDeviceInternationalPrefix(requireContext())
viewModel.internationalPrefix.postValue(prefix)
}
} }
override fun onResume() { override fun onResume() {

View file

@ -0,0 +1,100 @@
/*
* 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 <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.AssistantRegisterConfirmSmsCodeFragmentBinding
import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel
import org.linphone.ui.main.fragment.GenericFragment
@UiThread
class RegisterCodeConfirmationFragment : GenericFragment() {
companion object {
private const val TAG = "[Register Code Confirmation Fragment]"
}
private lateinit var binding: AssistantRegisterConfirmSmsCodeFragmentBinding
private val viewModel: AccountCreationViewModel by navGraphViewModels(
R.id.registerCodeConfirmationFragment
)
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = AssistantRegisterConfirmSmsCodeFragmentBinding.inflate(layoutInflater)
return binding.root
}
override fun goBack() {
findNavController().popBackStack()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
binding.setBackClickListener {
goBack()
}
viewModel.goToLoginPageEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Going to login fragment")
val action = RegisterCodeConfirmationFragmentDirections.actionRegisterCodeConfirmationFragmentToLoginFragment()
findNavController().navigate(action)
}
}
// 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()
}
}
}
}
}

View file

@ -22,14 +22,23 @@ package org.linphone.ui.assistant.fragment
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.navigation.fragment.findNavController 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.core.tools.Log
import org.linphone.databinding.AssistantRegisterFragmentBinding import org.linphone.databinding.AssistantRegisterFragmentBinding
import org.linphone.ui.assistant.model.ConfirmPhoneNumberDialogModel
import org.linphone.ui.assistant.viewmodel.AccountCreationViewModel
import org.linphone.ui.main.fragment.GenericFragment import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.DialogUtils
import org.linphone.utils.PhoneNumberUtils
@UiThread @UiThread
class RegisterFragment : GenericFragment() { class RegisterFragment : GenericFragment() {
@ -39,6 +48,10 @@ class RegisterFragment : GenericFragment() {
private lateinit var binding: AssistantRegisterFragmentBinding private lateinit var binding: AssistantRegisterFragmentBinding
private val viewModel: AccountCreationViewModel by navGraphViewModels(
R.id.registerFragment
)
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -56,6 +69,7 @@ class RegisterFragment : GenericFragment() {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
binding.lifecycleOwner = viewLifecycleOwner binding.lifecycleOwner = viewLifecycleOwner
binding.viewModel = viewModel
binding.setBackClickListener { binding.setBackClickListener {
goBack() goBack()
@ -65,6 +79,10 @@ class RegisterFragment : GenericFragment() {
goBack() goBack()
} }
binding.setShowCountryPickerClickListener {
// TODO FIXME
}
binding.setOpenSubscribeWebPageClickListener { binding.setOpenSubscribeWebPageClickListener {
try { try {
val url = "https://subscribe.linphone.org" val url = "https://subscribe.linphone.org"
@ -75,7 +93,66 @@ class RegisterFragment : GenericFragment() {
} }
} }
binding.setCreateClickListener { binding.username.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
viewModel.usernameError.value = ""
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
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 ->
showPhoneNumberConfirmationDialog(number)
}
}
viewModel.goToSmsCodeConfirmationViewEvent.observe(viewLifecycleOwner) {
it.consume {
Log.i("$TAG Going to SMS code confirmation fragment")
val action = RegisterFragmentDirections.actionRegisterFragmentToRegisterCodeConfirmationFragment()
findNavController().navigate(action)
}
}
coreContext.postOnCoreThread {
val prefix = PhoneNumberUtils.getDeviceInternationalPrefix(requireContext())
viewModel.internationalPrefix.postValue("+$prefix")
} }
} }
private fun showPhoneNumberConfirmationDialog(number: String) {
val model = ConfirmPhoneNumberDialogModel(number)
val dialog = DialogUtils.getAccountCreationPhoneNumberConfirmationDialog(
requireActivity(),
model
)
model.dismissEvent.observe(viewLifecycleOwner) {
it.consume {
dialog.dismiss()
}
}
model.confirmPhoneNumberEvent.observe(viewLifecycleOwner) {
it.consume {
viewModel.requestToken()
dialog.dismiss()
}
}
dialog.show()
}
} }

View file

@ -0,0 +1,46 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package org.linphone.ui.assistant.model
import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import org.linphone.utils.Event
class ConfirmPhoneNumberDialogModel @UiThread constructor(phoneNumber: String) {
val message = MutableLiveData<String>()
val dismissEvent = MutableLiveData<Event<Boolean>>()
val confirmPhoneNumberEvent = MutableLiveData<Event<Boolean>>()
init {
message.value = "Are you sure you want to use $phoneNumber phone number?"
}
@UiThread
fun dismiss() {
dismissEvent.value = Event(true)
}
@UiThread
fun confirmPhoneNumber() {
confirmPhoneNumberEvent.value = Event(true)
}
}

View file

@ -0,0 +1,409 @@
/*
* 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 <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.ViewModel
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.core.AccountCreator
import org.linphone.core.AccountCreatorListenerStub
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.tools.Log
import org.linphone.utils.Event
class AccountCreationViewModel @UiThread constructor() : ViewModel() {
companion object {
private const val TAG = "[Account Creation ViewModel]"
}
val username = MutableLiveData<String>()
val usernameError = MutableLiveData<String>()
val password = MutableLiveData<String>()
val phoneNumber = MutableLiveData<String>()
val phoneNumberError = MutableLiveData<String>()
val internationalPrefix = MutableLiveData<String>()
val showPassword = MutableLiveData<Boolean>()
val createEnabled = MediatorLiveData<Boolean>()
val smsCodeFirstDigit = MutableLiveData<String>()
val smsCodeSecondDigit = MutableLiveData<String>()
val smsCodeThirdDigit = MutableLiveData<String>()
val smsCodeLastDigit = MutableLiveData<String>()
val operationInProgress = MutableLiveData<Boolean>()
val normalizedPhoneNumberEvent = MutableLiveData<Event<String>>()
val goToSmsCodeConfirmationViewEvent = MutableLiveData<Event<Boolean>>()
val goToLoginPageEvent = MutableLiveData<Event<Boolean>>()
private var waitingForFlexiApiPushToken = false
private var waitForPushJob: Job? = null
private lateinit var accountCreator: AccountCreator
private val accountCreatorListener = object : AccountCreatorListenerStub() {
@WorkerThread
override fun onIsAccountExist(
creator: AccountCreator,
status: AccountCreator.Status,
response: String?
) {
Log.i("$TAG onIsAccountExist status [$status] ($response)")
when (status) {
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
operationInProgress.postValue(false)
createEnabled.postValue(false)
usernameError.postValue("Account already exists")
}
AccountCreator.Status.AccountNotExist -> {
operationInProgress.postValue(false)
checkPhoneNumber()
}
else -> {
operationInProgress.postValue(false)
createEnabled.postValue(false)
phoneNumberError.postValue(status.name)
}
}
}
@WorkerThread
override fun onIsAliasUsed(
creator: AccountCreator,
status: AccountCreator.Status,
response: String?
) {
Log.i("$TAG onIsAliasUsed status [$status] ($response)")
when (status) {
AccountCreator.Status.AliasExist, AccountCreator.Status.AliasIsAccount -> {
operationInProgress.postValue(false)
createEnabled.postValue(false)
phoneNumberError.postValue("Phone number already used")
}
AccountCreator.Status.AliasNotExist -> {
operationInProgress.postValue(false)
createAccount()
}
else -> {
operationInProgress.postValue(false)
createEnabled.postValue(false)
phoneNumberError.postValue(status.name)
}
}
}
@WorkerThread
override fun onCreateAccount(
creator: AccountCreator,
status: AccountCreator.Status,
response: String?
) {
Log.i("$TAG onCreateAccount status [$status] ($response)")
operationInProgress.postValue(false)
when (status) {
AccountCreator.Status.AccountCreated -> {
goToSmsCodeConfirmationViewEvent.postValue(Event(true))
}
else -> {
// TODO
}
}
}
@WorkerThread
override fun onActivateAccount(
creator: AccountCreator,
status: AccountCreator.Status,
response: String?
) {
Log.i("$TAG onActivateAccount status [$status] ($response)")
operationInProgress.postValue(false)
if (status == AccountCreator.Status.AccountActivated) {
Log.i("$TAG Account has been successfully activated, going to login page")
goToLoginPageEvent.postValue(Event(true))
} else {
// TODO
}
}
}
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()) {
Log.i("$TAG Extracted token [$token] from push payload")
accountCreator.token = token
checkUsername()
} 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 ->
accountCreator = core.createAccountCreator(core.accountCreatorUrl)
accountCreator.addListener(accountCreatorListener)
core.addListener(coreListener)
}
showPassword.value = false
createEnabled.addSource(username) {
createEnabled.value = isCreateButtonEnabled()
}
createEnabled.addSource(password) {
createEnabled.value = isCreateButtonEnabled()
}
createEnabled.addSource(internationalPrefix) {
createEnabled.value = isCreateButtonEnabled()
}
createEnabled.addSource(phoneNumber) {
createEnabled.value = isCreateButtonEnabled()
}
}
@UiThread
override fun onCleared() {
coreContext.postOnCoreThread { core ->
if (::accountCreator.isInitialized) {
accountCreator.removeListener(accountCreatorListener)
}
core.removeListener(coreListener)
}
waitForPushJob?.cancel()
super.onCleared()
}
@UiThread
fun confirmPhoneNumber() {
coreContext.postOnCoreThread {
if (::accountCreator.isInitialized) {
val prefix = internationalPrefix.value.orEmpty()
val digitsPrefix = if (prefix.startsWith("+")) {
prefix.substring(1)
} else {
prefix
}
val number = phoneNumber.value.orEmpty()
accountCreator.setPhoneNumber(number, digitsPrefix)
val normalizedPhoneNumber = accountCreator.phoneNumber
if (!normalizedPhoneNumber.isNullOrEmpty()) {
normalizedPhoneNumberEvent.postValue(Event(normalizedPhoneNumber))
} else {
Log.e(
"$TAG Failed to compute phone number using international prefix [$digitsPrefix] and number [$number]"
)
operationInProgress.postValue(false)
phoneNumberError.postValue("Wrong international prefix / local phone number")
}
} else {
Log.e("$TAG Account creator hasn't been initialized!")
}
}
}
@UiThread
fun requestToken() {
coreContext.postOnCoreThread {
requestFlexiApiToken()
}
}
@UiThread
fun toggleShowPassword() {
showPassword.value = showPassword.value == false
}
@UiThread
private fun isCreateButtonEnabled(): Boolean {
return username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() && phoneNumber.value.orEmpty().isNotEmpty() && internationalPrefix.value.orEmpty().isNotEmpty()
}
@UiThread
fun validateCode() {
operationInProgress.value = true
val code = "${smsCodeFirstDigit.value}${smsCodeSecondDigit.value}${smsCodeThirdDigit.value}${smsCodeLastDigit.value}"
Log.i("$TAG Activating account using code [$code]")
accountCreator.activationCode = code
coreContext.postOnCoreThread {
val status = accountCreator.activateAccount()
Log.i("$TAG activateAccount returned $status")
if (status != AccountCreator.Status.RequestOk) {
Log.e("$TAG Can't activate account [$status]")
operationInProgress.postValue(false)
}
}
}
@WorkerThread
private fun checkUsername() {
usernameError.postValue("")
accountCreator.username = username.value.orEmpty()
operationInProgress.postValue(true)
val status = accountCreator.isAccountExist
Log.i("$TAG isAccountExist returned $status")
if (status != AccountCreator.Status.RequestOk) {
Log.e("$TAG Can't check if account already exists [$status]")
operationInProgress.postValue(false)
}
}
@WorkerThread
private fun checkPhoneNumber() {
operationInProgress.postValue(true)
val status = accountCreator.isAliasUsed
Log.i("$TAG isAliasUsed returned $status")
if (status != AccountCreator.Status.RequestOk) {
Log.e("$TAG Can't check if phone number is already used [$status]")
operationInProgress.postValue(false)
}
}
@WorkerThread
private fun createAccount() {
operationInProgress.postValue(true)
accountCreator.password = password.value.orEmpty()
val status = accountCreator.createAccount()
Log.i("$TAG createAccount returned $status")
if (status != AccountCreator.Status.RequestOk) {
Log.e("$TAG Can't create account [$status]")
operationInProgress.postValue(false)
}
}
@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
}
val pushConfig = coreContext.core.pushNotificationConfig
if (pushConfig != null) {
Log.i(
"$TAG Found push notification info: provider [${pushConfig.provider}], param [${pushConfig.param}] and prid [${pushConfig.prid}]"
)
accountCreator.pnProvider = pushConfig.provider
accountCreator.pnParam = pushConfig.param
accountCreator.pnPrid = pushConfig.prid
// Request an auth token, will be sent by push
val result = accountCreator.requestAuthToken()
if (result == AccountCreator.Status.RequestOk) {
val waitFor = 5000
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 Failed to require a push with an auth token: [$result]")
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)
}
}

View file

@ -35,14 +35,17 @@ import org.linphone.core.RegistrationState
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.utils.Event import org.linphone.utils.Event
class AssistantViewModel @UiThread constructor() : ViewModel() { class AccountLoginViewModel @UiThread constructor() : ViewModel() {
companion object { companion object {
private const val TAG = "[Assistant ViewModel]" private const val TAG = "[Account Login ViewModel]"
} }
val username = MutableLiveData<String>() val username = MutableLiveData<String>()
val password = MutableLiveData<String>() val password = MutableLiveData<String>()
val internationalPrefix = MutableLiveData<String>()
val showPassword = MutableLiveData<Boolean>() val showPassword = MutableLiveData<Boolean>()
val loginEnabled = MediatorLiveData<Boolean>() val loginEnabled = MediatorLiveData<Boolean>()
@ -119,9 +122,21 @@ class AssistantViewModel @UiThread constructor() : ViewModel() {
val accountParams = core.createAccountParams() val accountParams = core.createAccountParams()
val identityAddress = Factory.instance().createAddress("sip:$user@$domain") val identityAddress = Factory.instance().createAddress("sip:$user@$domain")
accountParams.identityAddress = identityAddress accountParams.identityAddress = identityAddress
newlyCreatedAccount = core.createAccount(accountParams)
// TODO: set international prefix if detected val prefix = internationalPrefix.value.orEmpty()
if (prefix.isNotEmpty()) {
val prefixDigits = if (prefix.startsWith("+")) {
prefix.substring(1)
} else {
prefix
}
if (prefixDigits.isNotEmpty()) {
Log.i("$TAG Setting international prefix [$prefixDigits] in account params")
accountParams.internationalPrefix = prefixDigits
}
}
newlyCreatedAccount = core.createAccount(accountParams)
registrationInProgress.postValue(true) registrationInProgress.postValue(true)
core.addListener(coreListener) core.addListener(coreListener)

View file

@ -26,7 +26,9 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.annotation.UiThread import androidx.annotation.UiThread
@ -326,3 +328,36 @@ fun setInflatedViewStubLifecycleOwner(view: View, enable: Boolean) {
binding?.lifecycleOwner = view.context as? LifecycleOwner binding?.lifecycleOwner = view.context as? LifecycleOwner
} }
} }
@BindingAdapter("focusNextOnInput")
fun focusNextOnInput(editText: EditText, enabled: Boolean) {
if (!enabled) return
editText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
if (!s.isNullOrEmpty()) {
editText.onEditorAction(EditorInfo.IME_ACTION_NEXT)
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}
@BindingAdapter("validateOnInput")
fun validateOnInput(editText: EditText, onValidate: () -> (Unit)) {
editText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
if (!s.isNullOrEmpty()) {
editText.onEditorAction(EditorInfo.IME_ACTION_DONE)
onValidate.invoke()
}
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}

View file

@ -31,12 +31,14 @@ import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding import androidx.databinding.ViewDataBinding
import org.linphone.R import org.linphone.R
import org.linphone.databinding.DialogAssistantCreateAccountConfirmPhoneNumberBinding
import org.linphone.databinding.DialogCancelContactChangesBinding import org.linphone.databinding.DialogCancelContactChangesBinding
import org.linphone.databinding.DialogConfirmZrtpSasBinding import org.linphone.databinding.DialogConfirmZrtpSasBinding
import org.linphone.databinding.DialogContactConfirmTrustCallBinding import org.linphone.databinding.DialogContactConfirmTrustCallBinding
import org.linphone.databinding.DialogContactTrustProcessBinding import org.linphone.databinding.DialogContactTrustProcessBinding
import org.linphone.databinding.DialogPickNumberOrAddressBinding import org.linphone.databinding.DialogPickNumberOrAddressBinding
import org.linphone.databinding.DialogRemoveAllCallLogsBinding import org.linphone.databinding.DialogRemoveAllCallLogsBinding
import org.linphone.ui.assistant.model.ConfirmPhoneNumberDialogModel
import org.linphone.ui.main.calls.model.ConfirmationDialogModel import org.linphone.ui.main.calls.model.ConfirmationDialogModel
import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel import org.linphone.ui.main.contacts.model.NumberOrAddressPickerDialogModel
import org.linphone.ui.main.contacts.model.TrustCallDialogModel import org.linphone.ui.main.contacts.model.TrustCallDialogModel
@ -44,6 +46,22 @@ import org.linphone.ui.voip.model.ZrtpSasConfirmationDialogModel
class DialogUtils { class DialogUtils {
companion object { companion object {
@UiThread
fun getAccountCreationPhoneNumberConfirmationDialog(
context: Context,
viewModel: ConfirmPhoneNumberDialogModel
): Dialog {
val binding: DialogAssistantCreateAccountConfirmPhoneNumberBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.dialog_assistant_create_account_confirm_phone_number,
null,
false
)
binding.viewModel = viewModel
return getDialog(context, binding)
}
@UiThread @UiThread
fun getNumberOrAddressPickerDialog( fun getNumberOrAddressPickerDialog(
context: Context, context: Context,

View file

@ -19,12 +19,35 @@
*/ */
package org.linphone.utils package org.linphone.utils
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.provider.ContactsContract import android.provider.ContactsContract
import android.telephony.TelephonyManager
import androidx.annotation.AnyThread import androidx.annotation.AnyThread
import androidx.annotation.WorkerThread
import org.linphone.core.Factory
import org.linphone.core.tools.Log
class PhoneNumberUtils { class PhoneNumberUtils {
companion object { companion object {
private const val TAG = "[Phone Number Utils]"
@WorkerThread
fun getDeviceInternationalPrefix(context: Context): String? {
val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val countryIso = telephonyManager.networkCountryIso
for (dp in Factory.instance().dialPlans) {
if (dp.isoCountryCode.equals(countryIso, true)) {
val prefix = dp.countryCallingCode
Log.i(
"$TAG Found matching entry [$prefix] in dialplan for network country iso [$countryIso]"
)
return prefix
}
}
return null
}
@AnyThread @AnyThread
fun addressBookLabelTypeToVcardParamString(type: Int, default: String?): String { fun addressBookLabelTypeToVcardParamString(type: Int, default: String?): String {
return when (type) { return when (type) {

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true" android:color="@color/primary_color" />
<item android:color="@color/gray_1"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View file

@ -0,0 +1,13 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:name="vector"
android:width="2dp"
android:height="28dp"
android:viewportWidth="2"
android:viewportHeight="28">
<path
android:name="path"
android:pathData="M 0.627 0 L 1.773 0 L 1.773 28 L 0.627 28 L 0.627 0 Z"
android:fillColor="#4e6074"
android:strokeWidth="1"/>
</vector>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="15dp" />
<size android:width="73dp" android:height="73dp" />
<solid android:color="@color/white" />
<stroke android:width="1dp" android:color="@color/assistant_sms_confirmation_code_color"/>
</shape>

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="63dp" />
<solid android:color="@color/white"/>
<stroke android:width="1dp" android:color="@color/red_danger" />
</shape>

View file

@ -12,6 +12,9 @@
<variable <variable
name="registerClickListener" name="registerClickListener"
type="View.OnClickListener" /> type="View.OnClickListener" />
<variable
name="forgottenPasswordClickListener"
type="View.OnClickListener" />
<variable <variable
name="thirdPartySipAccountLoginClickListener" name="thirdPartySipAccountLoginClickListener"
type="View.OnClickListener" /> type="View.OnClickListener" />
@ -20,7 +23,7 @@
type="View.OnClickListener" /> type="View.OnClickListener" />
<variable <variable
name="viewModel" name="viewModel"
type="org.linphone.ui.assistant.viewmodel.AssistantViewModel" /> type="org.linphone.ui.assistant.viewmodel.AccountLoginViewModel" />
</data> </data>
<ScrollView <ScrollView
@ -97,6 +100,8 @@
android:textSize="14sp" android:textSize="14sp"
android:textColor="@color/gray_9" android:textColor="@color/gray_9"
android:background="@drawable/edit_text_background" android:background="@drawable/edit_text_background"
android:inputType="text"
android:hint="Username"
app:layout_constraintTop_toBottomOf="@id/username_label" app:layout_constraintTop_toBottomOf="@id/username_label"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
@ -128,6 +133,7 @@
android:textSize="14sp" android:textSize="14sp"
android:textColor="@color/gray_9" android:textColor="@color/gray_9"
android:background="@drawable/edit_text_background" android:background="@drawable/edit_text_background"
android:hint="Password"
android:inputType="@{viewModel.showPassword ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD, default=textPassword}" android:inputType="@{viewModel.showPassword ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD, default=textPassword}"
app:layout_constraintTop_toBottomOf="@id/password_label" app:layout_constraintTop_toBottomOf="@id/password_label"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -170,6 +176,7 @@
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:onClick="@{forgottenPasswordClickListener}"
style="@style/default_text_style_600" style="@style/default_text_style_600"
android:id="@+id/forgotten_password" android:id="@+id/forgotten_password"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

@ -0,0 +1,194 @@
<?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.AccountCreationViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:onClick="@{backClickListener}"
android:id="@+id/back"
android:layout_width="@dimen/icon_size"
android:layout_height="@dimen/icon_size"
android:layout_marginTop="7dp"
android:layout_marginStart="15dp"
android:src="@drawable/caret_left"
app:tint="@color/gray_1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:src="@drawable/header"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/back"
app:layout_constraintBottom_toBottomOf="@id/title"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:paddingBottom="30dp"
android:text="Register"
android:textSize="20sp"
android:textColor="@color/white"
app:layout_constraintTop_toTopOf="parent"
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="We have sent a verification code on your phone number “06945345435”.\n\nPlease enter the verification code below:"
android:textSize="14sp"
android:textColor="@color/gray_9"
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"
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: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="Wrong 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" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -5,11 +5,15 @@
<data> <data>
<import type="android.view.View" /> <import type="android.view.View" />
<import type="android.text.InputType" />
<variable <variable
name="backClickListener" name="backClickListener"
type="View.OnClickListener" /> type="View.OnClickListener" />
<variable <variable
name="createClickListener" name="createAccountClickListener"
type="View.OnClickListener" />
<variable
name="showCountryPickerClickListener"
type="View.OnClickListener" /> type="View.OnClickListener" />
<variable <variable
name="openSubscribeWebPageClickListener" name="openSubscribeWebPageClickListener"
@ -17,6 +21,9 @@
<variable <variable
name="loginClickListener" name="loginClickListener"
type="View.OnClickListener" /> type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.assistant.viewmodel.AccountCreationViewModel" />
</data> </data>
<ScrollView <ScrollView
@ -89,14 +96,29 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp" android:paddingEnd="20dp"
android:text="John Doe" android:text="@={viewModel.username, default=`johndoe`}"
android:textSize="14sp" android:textSize="14sp"
android:textColor="@color/gray_9" android:textColor="@color/gray_9"
android:background="@drawable/edit_text_background" android:background="@{viewModel.usernameError.length() > 0 ? @drawable/shape_edit_text_error_background : @drawable/edit_text_background, default=@drawable/edit_text_background}"
android:inputType=""
android:hint="Username"
app:layout_constraintTop_toBottomOf="@id/username_label" app:layout_constraintTop_toBottomOf="@id/username_label"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_600"
android:visibility="@{viewModel.usernameError.length() == 0 ? View.GONE : View.VISIBLE, default=gone}"
android:id="@+id/username_error"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@{viewModel.usernameError, default=`Error`}"
android:textSize="13sp"
android:textColor="@color/red_danger"
app:layout_constraintTop_toBottomOf="@id/username"
app:layout_constraintStart_toStartOf="@id/username"
app:layout_constraintEnd_toEndOf="@id/username"/>
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/phone_number_label" android:id="@+id/phone_number_label"
style="@style/default_text_style_700" style="@style/default_text_style_700"
@ -108,7 +130,8 @@
android:text="Phone Number*" android:text="Phone Number*"
android:textSize="13sp" android:textSize="13sp"
android:textColor="@color/gray_9" android:textColor="@color/gray_9"
app:layout_constraintTop_toBottomOf="@id/username" android:inputType="text"
app:layout_constraintTop_toBottomOf="@id/username_error"
app:layout_constraintStart_toStartOf="parent"/> app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
@ -116,18 +139,52 @@
android:id="@+id/phone_number" android:id="@+id/phone_number"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="50dp" android:layout_height="50dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:paddingStart="20dp" android:paddingStart="90dp"
android:paddingEnd="20dp" android:paddingEnd="20dp"
android:text="+33 6 01 02 03 04 05" android:text="@={viewModel.phoneNumber, default=`6 01 02 03 04 05`}"
android:textSize="14sp" android:textSize="14sp"
android:textColor="@color/gray_9" android:textColor="@color/gray_9"
android:background="@drawable/edit_text_background" android:background="@{viewModel.phoneNumberError.length() > 0 ? @drawable/shape_edit_text_error_background : @drawable/edit_text_background, default=@drawable/edit_text_background}"
android:inputType="phone"
android:drawableStart="@drawable/separator"
android:drawablePadding="10dp"
android:hint="Phone number"
app:layout_constraintTop_toBottomOf="@id/phone_number_label" app:layout_constraintTop_toBottomOf="@id/phone_number_label"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="@id/prefix"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{showCountryPickerClickListener}"
style="@style/default_text_style"
android:id="@+id/prefix"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:paddingStart="20dp"
android:text="@{viewModel.internationalPrefix, default=`+33`}"
android:textSize="14sp"
android:textColor="@color/gray_9"
android:gravity="center_vertical"
android:drawableEnd="@drawable/caret_down"
android:drawablePadding="10dp"
app:layout_constraintTop_toTopOf="@id/phone_number"
app:layout_constraintBottom_toBottomOf="@id/phone_number"
app:layout_constraintStart_toStartOf="parent" />
<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="@color/red_danger"
app:layout_constraintTop_toBottomOf="@id/prefix"
app:layout_constraintStart_toStartOf="@id/prefix"
app:layout_constraintEnd_toEndOf="@id/phone_number"/>
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/password_label" android:id="@+id/password_label"
style="@style/default_text_style_700" style="@style/default_text_style_700"
@ -139,7 +196,7 @@
android:text="Password*" android:text="Password*"
android:textSize="13sp" android:textSize="13sp"
android:textColor="@color/gray_9" android:textColor="@color/gray_9"
app:layout_constraintTop_toBottomOf="@id/phone_number" app:layout_constraintTop_toBottomOf="@id/phone_number_error"
app:layout_constraintStart_toStartOf="parent"/> app:layout_constraintStart_toStartOf="parent"/>
<androidx.appcompat.widget.AppCompatEditText <androidx.appcompat.widget.AppCompatEditText
@ -151,19 +208,32 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:paddingStart="20dp" android:paddingStart="20dp"
android:paddingEnd="20dp" android:paddingEnd="20dp"
android:text="John Doe" android:text="@={viewModel.password, default=`johndoe`}"
android:textSize="14sp" android:textSize="14sp"
android:textColor="@color/gray_9" android:textColor="@color/gray_9"
android:background="@drawable/edit_text_background" android:background="@drawable/edit_text_background"
android:inputType="textPassword" android:hint="Password"
android:drawableEnd="@drawable/eye" android:inputType="@{viewModel.showPassword ? InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD : InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD, default=textPassword}"
android:drawableTint="@color/gray_1"
app:layout_constraintTop_toBottomOf="@id/password_label" app:layout_constraintTop_toBottomOf="@id/password_label"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"/>
<ImageView
android:onClick="@{() -> viewModel.toggleShowPassword()}"
android:id="@+id/eye"
android:layout_width="@dimen/icon_size"
android:layout_height="0dp"
android:padding="4dp"
android:layout_marginEnd="20dp"
android:src="@{viewModel.showPassword ? @drawable/eye_slash : @drawable/eye, default=@drawable/eye}"
app:tint="@color/gray_1"
app:layout_constraintEnd_toEndOf="@id/password"
app:layout_constraintTop_toTopOf="@id/password"
app:layout_constraintBottom_toBottomOf="@id/password" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:onClick="@{createClickListener}" android:onClick="@{() -> viewModel.confirmPhoneNumber()}"
android:enabled="@{viewModel.createEnabled &amp;&amp; !viewModel.operationInProgress, default=false}"
style="@style/default_text_style_600" style="@style/default_text_style_600"
android:id="@+id/create" android:id="@+id/create"
android:layout_width="0dp" android:layout_width="0dp"

View file

@ -0,0 +1,126 @@
<?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">
<data>
<import type="android.view.View" />
<variable
name="viewModel"
type="org.linphone.ui.assistant.model.ConfirmPhoneNumberDialogModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> viewModel.dismiss()}"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:focusable="true">
<ImageView
android:id="@+id/dialog_background_shadow"
android:layout_width="0dp"
android:layout_height="0dp"
android:src="@drawable/shape_dialog_orange_shadow"
app:layout_constraintBottom_toBottomOf="@id/anchor"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintTop_toTopOf="@id/dialog_background" />
<ImageView
android:id="@+id/dialog_background"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="2dp"
android:src="@drawable/shape_dialog_background"
app:layout_constraintBottom_toBottomOf="@id/anchor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_800"
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:paddingTop="@dimen/dialog_top_bottom_margin"
android:text="Confirm phone number"
android:textSize="16sp"
app:layout_constraintVertical_chainStyle="packed"
app:layout_constraintBottom_toTopOf="@id/message"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toTopOf="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_marginStart="15dp"
android:layout_marginEnd="15dp"
android:layout_marginTop="10dp"
android:text="@{viewModel.message, default=`Are you sure you want to use this phone number?`}"
android:textSize="14sp"
app:layout_constraintBottom_toTopOf="@id/cancel"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/title" />
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.dismiss()}"
style="@style/default_text_style_600"
android:id="@+id/cancel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:paddingBottom="@dimen/primary_secondary_buttons_label_padding"
android:paddingTop="@dimen/primary_secondary_buttons_label_padding"
android:gravity="center"
android:background="@drawable/secondary_button_background"
android:text="Cancel"
android:textSize="18sp"
android:textColor="@color/secondary_button_label_color"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/message"
app:layout_constraintBottom_toTopOf="@id/confirm"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.confirmPhoneNumber()}"
style="@style/default_text_style_600"
android:id="@+id/confirm"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:paddingBottom="@dimen/primary_secondary_buttons_label_padding"
android:paddingTop="@dimen/primary_secondary_buttons_label_padding"
android:gravity="center"
android:background="@drawable/primary_button_background"
android:text="Continue"
android:textSize="18sp"
android:textColor="@color/primary_button_label_color"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintTop_toBottomOf="@id/cancel"
app:layout_constraintBottom_toTopOf="@id/anchor"/>
<View
android:id="@+id/anchor"
android:layout_width="wrap_content"
android:layout_height="@dimen/dialog_top_bottom_margin"
app:layout_constraintTop_toBottomOf="@id/confirm"
app:layout_constraintStart_toStartOf="@id/dialog_background"
app:layout_constraintEnd_toEndOf="@id/dialog_background"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -40,7 +40,16 @@
android:id="@+id/registerFragment" android:id="@+id/registerFragment"
android:name="org.linphone.ui.assistant.fragment.RegisterFragment" android:name="org.linphone.ui.assistant.fragment.RegisterFragment"
android:label="RegisterFragment" android:label="RegisterFragment"
tools:layout="@layout/assistant_register_fragment" /> tools:layout="@layout/assistant_register_fragment" >
<action
android:id="@+id/action_registerFragment_to_registerCodeConfirmationFragment"
app:destination="@id/registerCodeConfirmationFragment"
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 <fragment
android:id="@+id/qrCodeScannerFragment" android:id="@+id/qrCodeScannerFragment"
@ -54,4 +63,19 @@
android:label="ThirdPartySipAccountLoginFragment" android:label="ThirdPartySipAccountLoginFragment"
tools:layout="@layout/assistant_third_party_sip_account_login_fragment" /> tools:layout="@layout/assistant_third_party_sip_account_login_fragment" />
<fragment
android:id="@+id/registerCodeConfirmationFragment"
android:name="org.linphone.ui.assistant.fragment.RegisterCodeConfirmationFragment"
android:label="RegisterCodeConfirmationFragment"
tools:layout="@layout/assistant_register_confirm_sms_code_fragment" >
<action
android:id="@+id/action_registerCodeConfirmationFragment_to_loginFragment"
app:destination="@id/loginFragment"
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>
</navigation> </navigation>