Replaced AccountCreator by AccountManagerServices & using it to list account devices

This commit is contained in:
Sylvain Berfini 2024-06-07 16:24:03 +02:00
parent 54ee456f8e
commit 57f8ff3341
19 changed files with 466 additions and 378 deletions

View file

@ -28,6 +28,6 @@
</section>
<section name="sip">
<entry name="media_encryption" overwrite="true">zrtp</entry>
<entry name="media_encryption_mandatory" overwrite="true">1</entry>
<entry name="media_encryption_mandatory">1</entry>
</section>
</config>

View file

@ -22,4 +22,12 @@
<entry name="rtp_bundle" overwrite="true">1</entry>
<entry name="lime_server_url" overwrite="true"></entry>
</section>
<section name="nat_policy_default_values">
<entry name="stun_server" overwrite="true">stun.linphone.org</entry>
<entry name="protocols" overwrite="true">stun,ice</entry>
</section>
<section name="sip">
<entry name="media_encryption">srtp</entry>
<entry name="media_encryption_mandatory" overwrite="true">0</entry>
</section>
</config>

View file

@ -25,8 +25,6 @@ automatically_accept_direction=2 #receive only
[app]
tunnel=disabled
auto_start=1
record_aware=1
auto_download_incoming_voice_recordings=1
auto_download_incoming_icalendars=1

View file

@ -46,15 +46,10 @@ notify_each_friend_individually_when_presence_received=0
store_friends=0
[app]
activation_code_length=4
prefer_basic_chat_room=1
record_aware=1
[account_creator]
backend=1
# 1 means FlexiAPI, 0 is XMLRPC
url=https://subscribe.linphone.org/api/
# replace above URL by https://staging-subscribe.linphone.org/api/ for testing
[lime]
lime_update_threshold=86400
@ -62,9 +57,4 @@ lime_update_threshold=86400
[alerts]
alerts_enabled=1
[assistant]
algorithm=SHA-256
password_min_length=6
username_regex=^[a-z0-9+_.\-]*$
## End of factory rc

View file

@ -778,7 +778,7 @@ class CoreContext @UiThread constructor(val context: Context) : HandlerThread("C
val deviceName = AppUtils.getDeviceName(context)
val appName = context.getString(org.linphone.R.string.app_name)
val androidVersion = BuildConfig.VERSION_NAME
val userAgent = "$appName/$androidVersion ($deviceName) LinphoneSDK"
val userAgent = "${appName}Android/$androidVersion ($deviceName) LinphoneSDK"
val sdkVersion = context.getString(R.string.linphone_sdk_version)
val sdkBranch = context.getString(R.string.linphone_sdk_branch)
val sdkUserAgent = "$sdkVersion ($sdkBranch)"

View file

@ -69,9 +69,8 @@ class RegisterCodeConfirmationFragment : GenericFragment() {
viewModel.accountCreatedEvent.observe(viewLifecycleOwner) {
it.consume {
val identity = viewModel.username.value.orEmpty()
Log.i("$TAG Account [$identity] has been created")
val action = RegisterCodeConfirmationFragmentDirections.actionRegisterCodeConfirmationFragmentToLandingFragment()
findNavController().navigate(action)
Log.i("$TAG Account [$identity] has been created, leaving assistant")
requireActivity().finish()
}
}

View file

@ -217,7 +217,7 @@ class RegisterFragment : GenericFragment() {
model.confirmPhoneNumberEvent.observe(viewLifecycleOwner) {
it.consume {
viewModel.requestToken()
viewModel.startAccountCreation()
dialog.dismiss()
}
}

View file

@ -24,6 +24,7 @@ import androidx.annotation.WorkerThread
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import java.util.Locale
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -32,15 +33,16 @@ 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.AccountCreator.PhoneNumberStatus
import org.linphone.core.AccountCreator.UsernameStatus
import org.linphone.core.AccountCreatorListenerStub
import org.linphone.core.Account
import org.linphone.core.AccountManagerServices
import org.linphone.core.AccountManagerServicesRequest
import org.linphone.core.AccountManagerServicesRequestListenerStub
import org.linphone.core.AuthInfo
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
@ -51,6 +53,9 @@ import org.linphone.utils.LinphoneUtils
class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Account Creation ViewModel]"
private const val TIME_TO_WAIT_FOR_PUSH_NOTIFICATION_WITH_ACCOUNT_CREATION_TOKEN = 5000
private const val HASH_ALGORITHM = "SHA-256"
}
val username = MutableLiveData<String>()
@ -88,6 +93,7 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
val operationInProgress = MutableLiveData<Boolean>()
private var normalizedPhoneNumber: String? = null
val normalizedPhoneNumberEvent = MutableLiveData<Event<String>>()
val goToSmsCodeConfirmationViewEvent = MutableLiveData<Event<Boolean>>()
@ -101,131 +107,102 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
private var waitingForFlexiApiPushToken = false
private var waitForPushJob: Job? = null
private lateinit var accountCreator: AccountCreator
private lateinit var accountManagerServices: AccountManagerServices
private var accountCreationToken: String? = null
private val accountCreatorListener = object : AccountCreatorListenerStub() {
private var accountCreatedAuthInfo: AuthInfo? = null
private var accountCreated: Account? = null
private val accountManagerServicesListener = object : AccountManagerServicesRequestListenerStub() {
@WorkerThread
override fun onIsAccountExist(
creator: AccountCreator,
status: AccountCreator.Status,
response: String?
override fun onRequestSuccessful(
request: AccountManagerServicesRequest,
data: String?
) {
Log.i("$TAG onIsAccountExist status [$status] ($response)")
when (status) {
AccountCreator.Status.AccountExist, AccountCreator.Status.AccountExistWithAlias -> {
operationInProgress.postValue(false)
createEnabled.postValue(false)
val error = AppUtils.getString(
R.string.assistant_account_register_username_already_in_use_error
)
usernameError.postValue(error)
}
AccountCreator.Status.AccountNotExist -> {
operationInProgress.postValue(false)
checkPhoneNumber()
}
else -> {
Log.e("$TAG An unexpected error occurred!")
operationInProgress.postValue(false)
createEnabled.postValue(false)
phoneNumberError.postValue(
AppUtils.getString(
R.string.assistant_account_register_invalid_phone_number_error
)
)
}
}
}
@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)
val error = AppUtils.getString(
R.string.assistant_account_register_phone_number_already_in_use_error
)
phoneNumberError.postValue(error)
}
AccountCreator.Status.AliasNotExist -> {
operationInProgress.postValue(false)
createAccount()
}
else -> {
Log.e("$TAG An unexpected error occurred!")
operationInProgress.postValue(false)
createEnabled.postValue(false)
phoneNumberError.postValue(
AppUtils.getString(
R.string.assistant_account_register_invalid_phone_number_error
)
)
}
}
}
@WorkerThread
override fun onCreateAccount(
creator: AccountCreator,
status: AccountCreator.Status,
response: String?
) {
Log.i("$TAG onCreateAccount status [$status] ($response)")
accountCreator.token = null
Log.i("$TAG Request [$request] was successful, data is [$data]")
operationInProgress.postValue(false)
when (status) {
AccountCreator.Status.AccountCreated -> {
when (request.type) {
AccountManagerServicesRequest.Type.CreateAccountUsingToken -> {
if (!data.isNullOrEmpty()) {
storeAccountInCore(data)
sendCodeBySms()
} else {
Log.e(
"$TAG No data found for createAccountUsingToken request, can't continue!"
)
}
}
AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms -> {
goToSmsCodeConfirmationViewEvent.postValue(Event(true))
}
else -> {
Log.e("$TAG Account couldn't be created, an unexpected error occurred!")
errorHappenedEvent.postValue(
Event(
AppUtils.getFormattedString(
R.string.assistant_account_register_server_error,
status.toInt()
)
AccountManagerServicesRequest.Type.LinkPhoneNumberUsingCode -> {
val account = accountCreated
if (account != null) {
Log.i(
"$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] has been created & activated, setting it as default"
)
)
coreContext.core.defaultAccount = account
}
accountCreatedEvent.postValue(Event(true))
}
else -> { }
}
}
@WorkerThread
override fun onActivateAccount(
creator: AccountCreator,
status: AccountCreator.Status,
response: String?
override fun onRequestError(
request: AccountManagerServicesRequest,
statusCode: Int,
errorMessage: String?,
parameterErrors: Dictionary?
) {
Log.i("$TAG onActivateAccount status [$status] ($response)")
Log.e(
"$TAG Request [$request] returned an error with status code [$statusCode] and message [$errorMessage]"
)
operationInProgress.postValue(false)
if (status == AccountCreator.Status.AccountActivated) {
Log.i("$TAG Account has been successfully activated, going to login page")
accountCreatedEvent.postValue(Event(true))
} else {
Log.e("$TAG Account couldn't be activated, an unexpected error occurred!")
errorHappenedEvent.postValue(
if (!errorMessage.isNullOrEmpty()) {
showFormattedRedToastEvent.postValue(
Event(
AppUtils.getFormattedString(
R.string.assistant_account_register_server_error,
status.toInt()
Pair(
errorMessage,
R.drawable.warning_circle
)
)
)
}
for (parameter in parameterErrors?.keys.orEmpty()) {
val parameterErrorMessage = parameterErrors?.getString(parameter) ?: ""
when (parameter) {
"username" -> usernameError.postValue(parameterErrorMessage)
"password" -> passwordError.postValue(parameterErrorMessage)
"phone" -> phoneNumberError.postValue(parameterErrorMessage)
}
}
when (request.type) {
AccountManagerServicesRequest.Type.SendAccountCreationTokenByPush -> {
Log.w("$TAG Cancelling job waiting for push notification")
waitingForFlexiApiPushToken = false
waitForPushJob?.cancel()
}
AccountManagerServicesRequest.Type.SendPhoneNumberLinkingCodeBySms -> {
val authInfo = accountCreatedAuthInfo
if (authInfo != null) {
coreContext.core.removeAuthInfo(authInfo)
}
val account = accountCreated
if (account != null) {
coreContext.core.removeAccount(account)
}
createEnabled.postValue(true)
}
else -> {
createEnabled.postValue(true)
}
}
}
}
@ -253,9 +230,11 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
val token = customPayload.getString("token")
if (token.isNotEmpty()) {
Log.i("$TAG Extracted token [$token] from push payload")
accountCreator.token = token
checkUsername()
accountCreationToken = token
Log.i(
"$TAG Extracted token [$accountCreationToken] from push payload, creating account"
)
createAccount()
} else {
Log.e("$TAG Push payload JSON object has an empty 'token'!")
onFlexiApiTokenRequestError()
@ -292,8 +271,8 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
)
}
accountCreator = core.createAccountCreator(core.accountCreatorUrl)
accountCreator.addListener(accountCreatorListener)
accountManagerServices = core.createAccountManagerServices()
accountManagerServices.language = Locale.getDefault().language // Returns en, fr, etc...
core.addListener(coreListener)
}
@ -316,9 +295,6 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
@UiThread
override fun onCleared() {
coreContext.postOnCoreThread { core ->
if (::accountCreator.isInitialized) {
accountCreator.removeListener(accountCreatorListener)
}
core.removeListener(coreListener)
}
waitForPushJob?.cancel()
@ -327,79 +303,29 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
}
@UiThread
fun confirmPhoneNumber() {
fun phoneNumberConfirmedByUser() {
coreContext.postOnCoreThread {
if (::accountCreator.isInitialized) {
if (::accountManagerServices.isInitialized) {
val dialPlan = selectedDialPlan.value
val prefix = dialPlan?.countryCallingCode.orEmpty()
val digitsPrefix = if (prefix.startsWith("+")) {
prefix.substring(1)
} else {
prefix
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 status = accountCreator.setPhoneNumber(number, digitsPrefix)
Log.i("$TAG setPhoneNumber returned $status")
if (status == PhoneNumberStatus.Ok.toInt()) {
val normalizedPhoneNumber = accountCreator.phoneNumber
val message = coreContext.context.getString(
R.string.assistant_account_creation_sms_confirmation_explanation,
normalizedPhoneNumber
)
confirmationMessage.postValue(message)
Log.i(
"$TAG Normalized phone number from [$number] and prefix [$digitsPrefix] is [$normalizedPhoneNumber]"
)
if (!normalizedPhoneNumber.isNullOrEmpty()) {
normalizedPhoneNumberEvent.postValue(Event(normalizedPhoneNumber))
} else {
Log.e(
"$TAG Failed to compute phone number using international prefix [$digitsPrefix] and number [$number]"
)
val error = AppUtils.getString(
R.string.assistant_account_register_invalid_phone_number_error
)
phoneNumberError.postValue(error)
}
} else {
Log.e(
"$TAG Failed to set phone number [$number] and prefix [$digitsPrefix] into account creator!"
)
val error = when (status) {
PhoneNumberStatus.Invalid.toInt() -> {
AppUtils.getString(
R.string.assistant_account_register_invalid_phone_number_error
)
}
PhoneNumberStatus.InvalidCountryCode.toInt() -> {
AppUtils.getString(
R.string.assistant_account_register_invalid_phone_number_international_prefix_error
)
}
PhoneNumberStatus.TooLong.toInt() -> {
AppUtils.getString(
R.string.assistant_account_register_invalid_phone_number_too_long_error
)
}
PhoneNumberStatus.TooShort.toInt() -> {
AppUtils.getString(
R.string.assistant_account_register_invalid_phone_number_too_short_error
)
}
else -> {
AppUtils.getString(
R.string.assistant_account_register_invalid_phone_number_error
)
}
}
phoneNumberError.postValue(error)
}
val message = coreContext.context.getString(
R.string.assistant_account_creation_sms_confirmation_explanation,
formattedPhoneNumber
)
normalizedPhoneNumber = formattedPhoneNumber
confirmationMessage.postValue(message)
normalizedPhoneNumberEvent.postValue(Event(formattedPhoneNumber))
} else {
Log.e("$TAG Account creator hasn't been initialized!")
Log.e("$TAG Account manager services hasn't been initialized!")
errorHappenedEvent.postValue(
Event(AppUtils.getString(R.string.assistant_account_register_unexpected_error))
)
@ -408,16 +334,22 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
}
@UiThread
fun requestToken() {
fun startAccountCreation() {
operationInProgress.value = true
coreContext.postOnCoreThread {
if (accountCreator.token == null) {
if (accountCreationToken.isNullOrEmpty()) {
Log.i("$TAG We don't have a creation token, let's request one")
requestFlexiApiToken()
} else {
Log.i("$TAG We've already have a token [${accountCreator.token}], continuing")
checkUsername()
val authInfo = accountCreatedAuthInfo
if (authInfo != null) {
Log.i("$TAG Account has already been created, requesting SMS to be sent")
sendCodeBySms()
} else {
Log.i("$TAG We've already have a token [$accountCreationToken], continuing")
createAccount()
}
}
}
}
@ -434,141 +366,144 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
@UiThread
fun validateCode() {
usernameError.postValue("")
passwordError.postValue("")
phoneNumberError.postValue("")
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)
errorHappenedEvent.postValue(
Event(
AppUtils.getFormattedString(
R.string.assistant_account_register_server_error,
status.toInt()
)
)
val account = accountCreated
if (::accountManagerServices.isInitialized && account != null) {
val code =
"${smsCodeFirstDigit.value}${smsCodeSecondDigit.value}${smsCodeThirdDigit.value}${smsCodeLastDigit.value}"
val identity = account.params.identityAddress
if (identity != null) {
Log.i(
"$TAG Activating account using code [$code] for account [${identity.asStringUriOnly()}]"
)
val request = accountManagerServices.createLinkPhoneNumberToAccountUsingCodeRequest(
identity,
code
)
request.addListener(accountManagerServicesListener)
request.submit()
// Reset code
smsCodeFirstDigit.postValue("")
smsCodeSecondDigit.postValue("")
smsCodeThirdDigit.postValue("")
smsCodeLastDigit.postValue("")
}
}
}
@WorkerThread
private fun checkUsername() {
operationInProgress.postValue(true)
private fun sendCodeBySms() {
usernameError.postValue("")
val usernameStatus = accountCreator.setUsername(username.value.orEmpty().trim())
Log.i("$TAG setUsername returned $usernameStatus")
if (usernameStatus != UsernameStatus.Ok) {
val error = when (usernameStatus) {
UsernameStatus.InvalidCharacters, UsernameStatus.Invalid -> {
AppUtils.getString(
R.string.assistant_account_register_username_invalid_characters_error
)
}
UsernameStatus.TooShort -> {
AppUtils.getString(R.string.assistant_account_register_username_too_short_error)
}
UsernameStatus.TooLong -> {
AppUtils.getString(R.string.assistant_account_register_username_too_long_error)
}
else -> {
AppUtils.getString(R.string.assistant_account_register_username_error)
}
passwordError.postValue("")
phoneNumberError.postValue("")
val account = accountCreated
if (::accountManagerServices.isInitialized && account != null) {
val phoneNumberValue = normalizedPhoneNumber
if (phoneNumberValue.isNullOrEmpty()) {
Log.e("$TAG Phone number is null or empty, this shouldn't happen at this step!")
return
}
usernameError.postValue(error)
operationInProgress.postValue(false)
return
}
accountCreator.domain = corePreferences.defaultDomain
operationInProgress.postValue(true)
createEnabled.postValue(false)
val status = accountCreator.isAccountExist
Log.i("$TAG isAccountExist for username [${accountCreator.username}] returned $status")
if (status != AccountCreator.Status.RequestOk) {
Log.e("$TAG Can't check if account already exists [$status]")
operationInProgress.postValue(false)
errorHappenedEvent.postValue(
Event(
AppUtils.getFormattedString(
R.string.assistant_account_register_server_error,
status.toInt()
)
val identity = account.params.identityAddress
if (identity != null) {
Log.i(
"$TAG Account [${identity.asStringUriOnly()}] should now be created, asking account manager to send a confirmation code by SMS to [$phoneNumberValue]"
)
)
}
}
@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)
errorHappenedEvent.postValue(
Event(
AppUtils.getFormattedString(
R.string.assistant_account_register_server_error,
status.toInt()
)
val request = accountManagerServices.createSendPhoneNumberLinkingCodeBySmsRequest(
identity,
phoneNumberValue
)
)
request.addListener(accountManagerServicesListener)
request.submit()
}
}
}
@WorkerThread
private fun createAccount() {
operationInProgress.postValue(true)
usernameError.postValue("")
passwordError.postValue("")
phoneNumberError.postValue("")
val passwordStatus = accountCreator.setPassword(password.value.orEmpty().trim())
Log.i("$TAG setPassword returned $passwordStatus")
if (passwordStatus != AccountCreator.PasswordStatus.Ok) {
val error = when (passwordStatus) {
AccountCreator.PasswordStatus.InvalidCharacters -> {
AppUtils.getString(
R.string.assistant_account_register_password_invalid_characters_error
)
}
AccountCreator.PasswordStatus.TooShort -> {
AppUtils.getString(R.string.assistant_account_register_password_too_short)
}
AccountCreator.PasswordStatus.TooLong -> {
AppUtils.getString(R.string.assistant_account_register_password_too_long_error)
}
else -> {
AppUtils.getString(R.string.assistant_account_register_invalid_password_error)
}
if (::accountManagerServices.isInitialized) {
val token = accountCreationToken
if (token.isNullOrEmpty()) {
Log.e("$TAG No account creation token, can't create account!")
return
}
passwordError.postValue(error)
operationInProgress.postValue(false)
operationInProgress.postValue(true)
createEnabled.postValue(false)
val usernameValue = username.value
val passwordValue = password.value
if (usernameValue.isNullOrEmpty() || passwordValue.isNullOrEmpty()) {
Log.e("$TAG Either username [$usernameValue] or password is null or empty!")
return
}
Log.i(
"$TAG Account creation token is [$token], creating account with username [$usernameValue] and algorithm SHA-256"
)
val request = accountManagerServices.createNewAccountUsingTokenRequest(
usernameValue,
passwordValue,
HASH_ALGORITHM,
token
)
request.addListener(accountManagerServicesListener)
request.submit()
}
}
@WorkerThread
private fun storeAccountInCore(identity: String) {
val passwordValue = password.value
val core = coreContext.core
val sipIdentity = Factory.instance().createAddress(identity)
if (sipIdentity == null) {
Log.e("$TAG Failed to create address from SIP Identity [$identity]!")
return
}
val status = accountCreator.createAccount() // TODO FIXME: use createPushAccount instead ?
Log.i("$TAG createAccount returned $status")
if (status != AccountCreator.Status.RequestOk) {
Log.e("$TAG Can't create account [$status]")
operationInProgress.postValue(false)
errorHappenedEvent.postValue(
Event(
AppUtils.getFormattedString(
R.string.assistant_account_register_server_error,
status.toInt()
)
)
// We need to have an AuthInfo for newly created account to authorize phone number linking request
val authInfo = Factory.instance().createAuthInfo(
sipIdentity.username.orEmpty(),
null,
passwordValue,
null,
null,
sipIdentity.domain
)
core.addAuthInfo(authInfo)
Log.i("$TAG Auth info for SIP identity [${sipIdentity.asStringUriOnly()}] created & added")
val dialPlan = selectedDialPlan.value
val accountParams = core.createAccountParams()
accountParams.identityAddress = sipIdentity
if (dialPlan != null) {
Log.i(
"$TAG Setting international prefix [${dialPlan.internationalCallPrefix}] and country [${dialPlan.isoCountryCode}] to account params"
)
} else {
Log.i("$TAG createAccount consumed our token, setting it to null")
accountCreator.token = null
accountParams.internationalPrefix = dialPlan.internationalCallPrefix
accountParams.internationalPrefixIsoCountryCode = dialPlan.isoCountryCode
}
val account = core.createAccount(accountParams)
core.addAccount(account)
Log.i("$TAG Account for SIP identity [${sipIdentity.asStringUriOnly()}] created & added")
accountCreatedAuthInfo = authInfo
accountCreated = account
}
@WorkerThread
@ -582,39 +517,46 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
}
operationInProgress.postValue(true)
createEnabled.postValue(false)
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
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 result = accountCreator.requestAuthToken()
if (result == AccountCreator.Status.RequestOk) {
val waitFor = 5000
waitingForFlexiApiPushToken = true
waitForPushJob?.cancel()
val request = accountManagerServices.createSendAccountCreationTokenByPushRequest(
provider,
param,
prid
)
request.addListener(accountManagerServicesListener)
request.submit()
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()
}
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 Failed to require a push with an auth token: [$result]")
onFlexiApiTokenRequestError()
}
} else {
Log.e("$TAG No push configuration object in Core, shouldn't happen!")

View file

@ -20,15 +20,49 @@
package org.linphone.ui.main.settings.model
import androidx.annotation.WorkerThread
import java.time.ZonedDateTime
import org.linphone.core.AccountDevice
import org.linphone.core.tools.Log
import org.linphone.utils.TimestampUtils
class AccountDeviceModel @WorkerThread constructor(
val name: String,
val lastConnectionDate: String,
val lastConnectionTime: String,
private val onRemove: () -> (Unit)
private val accountDevice: AccountDevice,
private val onRemove: (accountDevice: AccountDevice) -> (Unit)
) {
companion object {
const val TAG = "[Account Device Model]"
}
val name = accountDevice.name
val timestamp = if (accountDevice.lastUpdateTimestamp == 0L) {
Log.w("$TAG SDK failed to parse [${accountDevice.lastUpdateTimestamp}] as time_t!")
try {
ZonedDateTime.parse(accountDevice.lastUpdateTime).toEpochSecond()
} catch (e: Exception) {
Log.e("$TAG Failed to parse [${accountDevice.lastUpdateTime}] as ZonedDateTime!")
0L
}
} else {
accountDevice.lastUpdateTimestamp
}
val lastConnectionDate = TimestampUtils.toString(
timestamp,
onlyDate = true,
shortDate = true,
hideYear = false
)
val lastConnectionTime = TimestampUtils.timeToString(timestamp)
val isMobileDevice = accountDevice.userAgent.contains("LinphoneAndroid") || accountDevice.userAgent.contains(
"LinphoneiOS"
)
init {
Log.d(
"$TAG Device's [$name] last update timestamp is [$timestamp] ($lastConnectionDate - $lastConnectionTime)"
)
}
fun removeDevice() {
onRemove()
onRemove(accountDevice)
}
}

View file

@ -20,11 +20,19 @@
package org.linphone.ui.main.settings.viewmodel
import androidx.annotation.UiThread
import androidx.annotation.WorkerThread
import androidx.lifecycle.MutableLiveData
import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.core.Account
import org.linphone.core.AccountDevice
import org.linphone.core.AccountManagerServices
import org.linphone.core.AccountManagerServicesRequest
import org.linphone.core.AccountManagerServicesRequestListenerStub
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
@ -62,6 +70,8 @@ class AccountProfileViewModel @UiThread constructor() : GenericViewModel() {
val expandDevices = MutableLiveData<Boolean>()
val devicesAvailable = MutableLiveData<Boolean>()
val hideAccountSettings = MutableLiveData<Boolean>()
val accountRemovedEvent: MutableLiveData<Event<Boolean>> by lazy {
@ -70,9 +80,72 @@ class AccountProfileViewModel @UiThread constructor() : GenericViewModel() {
private lateinit var account: Account
private lateinit var accountManagerServices: AccountManagerServices
private val accountManagerServicesListener = object : AccountManagerServicesRequestListenerStub() {
@WorkerThread
override fun onDevicesListFetched(
request: AccountManagerServicesRequest,
accountDevices: Array<out AccountDevice>
) {
Log.i("$TAG Fetched [${accountDevices.size}] devices for our account")
val devicesList = arrayListOf<AccountDeviceModel>()
for (accountDevice in accountDevices) {
devicesList.add(
AccountDeviceModel(accountDevice) { device ->
if (::accountManagerServices.isInitialized) {
val identityAddress = account.params.identityAddress
if (identityAddress != null) {
Log.i(
"$TAG Removing device with name [${device.name}] and uuid [${device.uuid}]"
)
val deleteRequest = accountManagerServices.createDeleteDeviceRequest(
identityAddress,
device
)
deleteRequest.addListener(this)
deleteRequest.submit()
} else {
Log.e("$TAG Account identity address is null, can't delete device!")
}
}
}
)
}
devices.postValue(devicesList)
}
@WorkerThread
override fun onRequestError(
request: AccountManagerServicesRequest,
statusCode: Int,
errorMessage: String?,
parameterErrors: Dictionary?
) {
Log.e(
"$TAG Request [${request.type}] returned an error with status code [$statusCode] and message [$errorMessage]"
)
if (!errorMessage.isNullOrEmpty()) {
when (request.type) {
AccountManagerServicesRequest.Type.GetDevicesList, AccountManagerServicesRequest.Type.DeleteDevice -> {
showFormattedRedToastEvent.postValue(
Event(
Pair(
errorMessage,
R.drawable.warning_circle
)
)
)
}
else -> {}
}
}
}
}
init {
expandDetails.value = true
expandDevices.value = false // TODO: set to true when feature will be available
expandDevices.value = false
coreContext.postOnCoreThread { core ->
hideAccountSettings.postValue(corePreferences.hideAccountSettings)
@ -92,7 +165,7 @@ class AccountProfileViewModel @UiThread constructor() : GenericViewModel() {
override fun onCleared() {
super.onCleared()
coreContext.postOnCoreThread { core ->
coreContext.postOnCoreThread {
accountModel.value?.destroy()
}
}
@ -113,9 +186,30 @@ class AccountProfileViewModel @UiThread constructor() : GenericViewModel() {
sipAddress.postValue(account.params.identityAddress?.asStringUriOnly())
displayName.postValue(account.params.identityAddress?.displayName)
val devicesList = arrayListOf<AccountDeviceModel>()
// TODO FIXME: use real devices list from API, not implemented yet
devices.postValue(devicesList)
val identityAddress = account.params.identityAddress
if (identityAddress != null) {
val domain = identityAddress.domain
val defaultDomain = corePreferences.defaultDomain
devicesAvailable.postValue(domain == defaultDomain)
if (domain == defaultDomain) {
Log.i(
"$TAG Request list of known devices for account [${identityAddress.asStringUriOnly()}]"
)
accountManagerServices = core.createAccountManagerServices()
accountManagerServices.language = Locale.getDefault().language // Returns en, fr, etc...
val request = accountManagerServices.createGetDevicesListRequest(
identityAddress
)
request.addListener(accountManagerServicesListener)
request.submit()
} else {
Log.i(
"$TAG Account with domain [$domain] can't get devices list, only works with [$defaultDomain] domain"
)
}
} else {
Log.e("$TAG No identity address found!")
}
val prefix = account.params.internationalPrefix
val isoCountryCode = account.params.internationalPrefixIsoCountryCode

View file

@ -253,14 +253,12 @@ class MainViewModel @UiThread constructor() : ViewModel() {
@WorkerThread
override fun onAccountRemoved(core: Core, account: Account) {
accountsFound -= 1
if (core.defaultAccount == null) {
Log.i(
"$TAG Default account was removed, setting first available account (if any) as default"
)
core.defaultAccount = core.accountList.firstOrNull()
}
Log.w(
"$TAG Account [${account.params.identityAddress?.asStringUriOnly()}] has been removed!"
)
removeAlert(NON_DEFAULT_ACCOUNT_NOT_CONNECTED)
core.refreshRegisters()
computeNonDefaultAccountNotificationsCount()
}
}

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="M208,40H48A24,24 0,0 0,24 64V176a24,24 0,0 0,24 24h72v16H96a8,8 0,0 0,0 16h64a8,8 0,0 0,0 -16H136V200h72a24,24 0,0 0,24 -24V64A24,24 0,0 0,208 40ZM48,56H208a8,8 0,0 1,8 8v80H40V64A8,8 0,0 1,48 56ZM208,184H48a8,8 0,0 1,-8 -8V160H216v16A8,8 0,0 1,208 184Z"
android:fillColor="#4e6074"/>
</vector>

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="M176,16L80,16A24,24 0,0 0,56 40L56,216a24,24 0,0 0,24 24h96a24,24 0,0 0,24 -24L200,40A24,24 0,0 0,176 16ZM184,216a8,8 0,0 1,-8 8L80,224a8,8 0,0 1,-8 -8L72,40a8,8 0,0 1,8 -8h96a8,8 0,0 1,8 8ZM140,60a12,12 0,1 1,-12 -12A12,12 0,0 1,140 60Z"
android:fillColor="#4e6074"/>
</vector>

View file

@ -22,12 +22,15 @@
android:id="@+id/name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="23dp"
android:layout_marginStart="15dp"
android:layout_marginTop="34dp"
android:layout_marginEnd="5dp"
android:text="@{model.name, default=`Pixel 6 Pro`}"
android:maxLines="1"
android:ellipsize="end"
android:drawableStart="@{model.isMobileDevice ? @drawable/device_mobile_camera : @drawable/desktop, default=@drawable/device_mobile_camera}"
android:drawablePadding="6dp"
app:drawableTint="?attr/color_main2_700"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/remove"/>
@ -60,29 +63,30 @@
android:id="@+id/last_connection"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="23dp"
android:layout_marginTop="30dp"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:text="@string/manage_account_device_last_connection"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/last_connection_date"/>
app:layout_constraintEnd_toEndOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/last_connection_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginStart="25dp"
android:layout_marginTop="10dp"
android:text="@{model.lastConnectionDate, default=`03/10/2023`}"
android:textSize="14sp"
android:textColor="?attr/color_main2_600"
android:drawableStart="@drawable/calendar_blank"
android:drawablePadding="6dp"
app:layout_constraintTop_toTopOf="@id/last_connection"
app:layout_constraintBottom_toBottomOf="@id/last_connection"
app:layout_constraintStart_toEndOf="@id/last_connection"
app:drawableTint="?attr/color_main2_700"
app:layout_constraintTop_toBottomOf="@id/last_connection"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/last_connection_time"/>
<androidx.appcompat.widget.AppCompatTextView
@ -90,16 +94,17 @@
android:id="@+id/last_connection_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:text="@{model.lastConnectionTime, default=`9h25`}"
android:textSize="14sp"
android:textColor="?attr/color_main2_600"
android:drawableStart="@drawable/clock"
android:drawablePadding="6dp"
app:layout_constraintTop_toTopOf="@id/last_connection"
app:layout_constraintBottom_toBottomOf="@id/last_connection"
app:layout_constraintStart_toEndOf="@id/last_connection_date"
app:layout_constraintEnd_toEndOf="parent"/>
app:drawableTint="?attr/color_main2_700"
app:layout_constraintTop_toBottomOf="@id/last_connection"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/last_connection_date"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -379,9 +379,9 @@
android:layout_marginEnd="26dp"
android:layout_marginTop="32dp"
android:text="@string/manage_account_devices_title"
android:visibility="@{viewModel.devicesAvailable ? View.VISIBLE : View.GONE, default=gone}"
android:drawableEnd="@{viewModel.expandDevices ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}"
android:drawableTint="@color/gray_main2_600"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/connection_background"/>
@ -400,9 +400,10 @@
android:paddingEnd="16dp"
android:paddingBottom="20dp"
android:background="@drawable/shape_squircle_white_background"
android:visibility="@{viewModel.expandDevices ? View.VISIBLE : View.GONE, default=gone}"
android:visibility="@{viewModel.devicesAvailable &amp;&amp; viewModel.expandDevices ? View.VISIBLE : View.GONE, default=gone}"
app:entries="@{viewModel.devices}"
app:layout="@{@layout/account_profile_device_list_cell}"
app:layout_constraintHeight_min="80dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/devices" />

View file

@ -178,6 +178,7 @@
<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"

View file

@ -268,7 +268,7 @@
app:layout_constraintBottom_toBottomOf="@id/password" />
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.confirmPhoneNumber()}"
android:onClick="@{() -> viewModel.phoneNumberConfirmedByUser()}"
android:enabled="@{viewModel.createEnabled &amp;&amp; viewModel.pushNotificationsAvailable &amp;&amp; !viewModel.operationInProgress, default=false}"
style="@style/primary_button_label_style"
android:id="@+id/create"

View file

@ -69,7 +69,7 @@
<!-- Generic toasts -->
<string name="sip_address_copied_to_clipboard_toast">L\'adresse SIP a été copiée</string>
<string name="new_account_configured_toast">Connexion réussie</string>
<string name="new_account_configured_toast">Nouveau compte ajouté</string>
<string name="default_account_connection_state_error_toast">Erreur de connexion !</string>
<string name="file_successfully_exported_to_media_store_toast">Le media a été exporté</string>
<string name="export_file_to_media_store_error_toast">Le media n\'a pas pû être exporté !</string>

View file

@ -105,7 +105,7 @@
<!-- Generic toasts -->
<string name="sip_address_copied_to_clipboard_toast">SIP address copied into clipboard</string>
<string name="new_account_configured_toast">Connection successful</string>
<string name="new_account_configured_toast">New account configured</string>
<string name="default_account_connection_state_error_toast">Connection error!</string>
<string name="file_successfully_exported_to_media_store_toast">File has been exported to native gallery</string>
<string name="export_file_to_media_store_error_toast">Error trying to export file to native gallery</string>