From 57f8ff33412a7c410c23528b44688af04fb88bbf Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Fri, 7 Jun 2024 16:24:03 +0200 Subject: [PATCH] Replaced AccountCreator by AccountManagerServices & using it to list account devices --- .../assets/assistant_linphone_default_values | 2 +- .../assistant_third_party_default_values | 8 + app/src/main/assets/linphonerc_default | 2 - app/src/main/assets/linphonerc_factory | 10 - .../java/org/linphone/core/CoreContext.kt | 2 +- .../RegisterCodeConfirmationFragment.kt | 5 +- .../ui/assistant/fragment/RegisterFragment.kt | 2 +- .../viewmodel/AccountCreationViewModel.kt | 588 ++++++++---------- .../main/settings/model/AccountDeviceModel.kt | 44 +- .../viewmodel/AccountProfileViewModel.kt | 104 +++- .../ui/main/viewmodel/MainViewModel.kt | 14 +- app/src/main/res/drawable/desktop.xml | 9 + .../res/drawable/device_mobile_camera.xml | 9 + .../account_profile_device_list_cell.xml | 33 +- .../res/layout/account_profile_fragment.xml | 5 +- ...ant_register_confirm_sms_code_fragment.xml | 1 + .../layout/assistant_register_fragment.xml | 2 +- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 19 files changed, 466 insertions(+), 378 deletions(-) create mode 100644 app/src/main/res/drawable/desktop.xml create mode 100644 app/src/main/res/drawable/device_mobile_camera.xml 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 @@
zrtp - 1 + 1
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 +
+
+ srtp + 0 +
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