diff --git a/app/src/main/assets/assistant_linphone_default_values b/app/src/main/assets/assistant_linphone_default_values
index 474fdd310..3081b5caa 100644
--- a/app/src/main/assets/assistant_linphone_default_values
+++ b/app/src/main/assets/assistant_linphone_default_values
@@ -28,6 +28,6 @@
diff --git a/app/src/main/assets/assistant_third_party_default_values b/app/src/main/assets/assistant_third_party_default_values
index af3833fcc..b3e35b151 100644
--- a/app/src/main/assets/assistant_third_party_default_values
+++ b/app/src/main/assets/assistant_third_party_default_values
@@ -22,4 +22,12 @@
1
+
+ stun.linphone.org
+ stun,ice
+
+
diff --git a/app/src/main/assets/linphonerc_default b/app/src/main/assets/linphonerc_default
index 5f8eeffa7..13cfb6207 100644
--- a/app/src/main/assets/linphonerc_default
+++ b/app/src/main/assets/linphonerc_default
@@ -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
diff --git a/app/src/main/assets/linphonerc_factory b/app/src/main/assets/linphonerc_factory
index b2e06c9d1..2ae29b3f9 100644
--- a/app/src/main/assets/linphonerc_factory
+++ b/app/src/main/assets/linphonerc_factory
@@ -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
diff --git a/app/src/main/java/org/linphone/core/CoreContext.kt b/app/src/main/java/org/linphone/core/CoreContext.kt
index 85e00409e..360bdbee9 100644
--- a/app/src/main/java/org/linphone/core/CoreContext.kt
+++ b/app/src/main/java/org/linphone/core/CoreContext.kt
@@ -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)"
diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt
index 3132b03bd..083a7c80e 100644
--- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt
+++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterCodeConfirmationFragment.kt
@@ -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()
}
}
diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt
index 73a24c142..6a6fcfb4a 100644
--- a/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt
+++ b/app/src/main/java/org/linphone/ui/assistant/fragment/RegisterFragment.kt
@@ -217,7 +217,7 @@ class RegisterFragment : GenericFragment() {
model.confirmPhoneNumberEvent.observe(viewLifecycleOwner) {
it.consume {
- viewModel.requestToken()
+ viewModel.startAccountCreation()
dialog.dismiss()
}
}
diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt
index 59ee82fa9..708df4812 100644
--- a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AccountCreationViewModel.kt
@@ -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()
@@ -88,6 +93,7 @@ class AccountCreationViewModel @UiThread constructor() : GenericViewModel() {
val operationInProgress = MutableLiveData()
+ private var normalizedPhoneNumber: String? = null
val normalizedPhoneNumberEvent = MutableLiveData>()
val goToSmsCodeConfirmationViewEvent = MutableLiveData>()
@@ -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!")
diff --git a/app/src/main/java/org/linphone/ui/main/settings/model/AccountDeviceModel.kt b/app/src/main/java/org/linphone/ui/main/settings/model/AccountDeviceModel.kt
index b931afaca..2783d73c0 100644
--- a/app/src/main/java/org/linphone/ui/main/settings/model/AccountDeviceModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/settings/model/AccountDeviceModel.kt
@@ -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)
}
}
diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt
index 31f9333fd..62af442cc 100644
--- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/AccountProfileViewModel.kt
@@ -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()
+ val devicesAvailable = MutableLiveData()
+
val hideAccountSettings = MutableLiveData()
val accountRemovedEvent: MutableLiveData> 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
+ ) {
+ Log.i("$TAG Fetched [${accountDevices.size}] devices for our account")
+ val devicesList = arrayListOf()
+ 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()
- // 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
diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt
index b2ed30681..0dfe1f9ce 100644
--- a/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/viewmodel/MainViewModel.kt
@@ -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()
}
}
diff --git a/app/src/main/res/drawable/desktop.xml b/app/src/main/res/drawable/desktop.xml
new file mode 100644
index 000000000..ecfce9897
--- /dev/null
+++ b/app/src/main/res/drawable/desktop.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/device_mobile_camera.xml b/app/src/main/res/drawable/device_mobile_camera.xml
new file mode 100644
index 000000000..bca10a2a3
--- /dev/null
+++ b/app/src/main/res/drawable/device_mobile_camera.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/layout/account_profile_device_list_cell.xml b/app/src/main/res/layout/account_profile_device_list_cell.xml
index 6a80897a0..647be35a1 100644
--- a/app/src/main/res/layout/account_profile_device_list_cell.xml
+++ b/app/src/main/res/layout/account_profile_device_list_cell.xml
@@ -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" />
+ 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"/>
diff --git a/app/src/main/res/layout/account_profile_fragment.xml b/app/src/main/res/layout/account_profile_fragment.xml
index 196ac9a6c..f220fed9a 100644
--- a/app/src/main/res/layout/account_profile_fragment.xml
+++ b/app/src/main/res/layout/account_profile_fragment.xml
@@ -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 && 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" />
diff --git a/app/src/main/res/layout/assistant_register_confirm_sms_code_fragment.xml b/app/src/main/res/layout/assistant_register_confirm_sms_code_fragment.xml
index ee0a90895..4d106db99 100644
--- a/app/src/main/res/layout/assistant_register_confirm_sms_code_fragment.xml
+++ b/app/src/main/res/layout/assistant_register_confirm_sms_code_fragment.xml
@@ -178,6 +178,7 @@
L\'adresse SIP a été copiée
- Connexion réussie
+ Nouveau compte ajouté
Erreur de connexion !
Le media a été exporté
Le media n\'a pas pû être exporté !
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ae7e94a85..3dad41f6c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -105,7 +105,7 @@
SIP address copied into clipboard
- Connection successful
+ New account configured
Connection error!
File has been exported to native gallery
Error trying to export file to native gallery