diff --git a/app/src/main/java/org/linphone/contacts/ContactsManager.kt b/app/src/main/java/org/linphone/contacts/ContactsManager.kt index 852c4d3f0..3e29dd00d 100644 --- a/app/src/main/java/org/linphone/contacts/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contacts/ContactsManager.kt @@ -22,9 +22,13 @@ package org.linphone.contacts import androidx.loader.app.LoaderManager import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.core.Friend +import org.linphone.core.tools.Log import org.linphone.ui.main.MainActivity +import org.linphone.utils.LinphoneUtils class ContactsManager { + val localFriends = arrayListOf() + private val listeners = arrayListOf() fun loadContacts(activity: MainActivity) { @@ -54,6 +58,8 @@ class ContactsManager { fun onContactsLoaded() { // UI thread coreContext.postOnCoreThread { + updateLocalContacts() + for (listener in listeners) { listener.onContactsLoaded() } @@ -65,8 +71,28 @@ class ContactsManager { return coreContext.core.defaultFriendList?.findFriendByRefKey(id) } + fun updateLocalContacts() { + // Core thread + Log.i("[Contacts Manager] Updating local contact(s)") + localFriends.clear() + + for (account in coreContext.core.accountList) { + val friend = coreContext.core.createFriend() + friend.name = LinphoneUtils.getDisplayName(account.params.identityAddress) + + val address = account.params.identityAddress ?: continue + friend.address = address + + Log.i( + "[Contacts Manager] Local contact created for account [${address.asString()}] and picture [${friend.photo}]" + ) + localFriends.add(friend) + } + } + fun onCoreStarted() { // Core thread + updateLocalContacts() } fun onCoreStopped() { diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index a730ebf3c..999c07bef 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -51,15 +51,22 @@ class CorePreferences constructor(private val context: Context) { editor.apply() } + val defaultDomain: String + get() = config.getString("app", "default_domain", "sip.linphone.org")!! + val configPath: String get() = context.filesDir.absolutePath + "/.linphonerc" val factoryConfigPath: String get() = context.filesDir.absolutePath + "/linphonerc" + val linphoneDefaultValuesPath: String + get() = context.filesDir.absolutePath + "/assistant_linphone_default_values" + fun copyAssetsFromPackage() { copy("linphonerc_default", configPath) copy("linphonerc_factory", factoryConfigPath, true) + copy("assistant_linphone_default_values", linphoneDefaultValuesPath, true) } private fun copy(from: String, to: String, overrideIfExists: Boolean = false) { diff --git a/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt b/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt index 296611d94..0ddb8a18f 100644 --- a/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt +++ b/app/src/main/java/org/linphone/ui/assistant/fragment/LoginFragment.kt @@ -24,12 +24,19 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.navigation.fragment.findNavController +import androidx.navigation.navGraphViewModels +import org.linphone.R import org.linphone.databinding.AssistantLoginFragmentBinding +import org.linphone.ui.assistant.viewmodel.AssistantViewModel import org.linphone.ui.main.fragment.GenericFragment class LoginFragment : GenericFragment() { private lateinit var binding: AssistantLoginFragmentBinding + private val viewModel: AssistantViewModel by navGraphViewModels( + R.id.loginFragment + ) + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -47,10 +54,17 @@ class LoginFragment : GenericFragment() { super.onViewCreated(view, savedInstanceState) binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel binding.setRegisterClickListener { val action = LoginFragmentDirections.actionLoginFragmentToRegisterFragment() findNavController().navigate(action) } + + viewModel.accountLoggedInEvent.observe(viewLifecycleOwner) { + it.consume { + goBack() + } + } } } diff --git a/app/src/main/java/org/linphone/ui/assistant/viewmodel/AssistantViewModel.kt b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AssistantViewModel.kt new file mode 100644 index 000000000..027afcc9c --- /dev/null +++ b/app/src/main/java/org/linphone/ui/assistant/viewmodel/AssistantViewModel.kt @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.assistant.viewmodel + +import androidx.lifecycle.MediatorLiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.LinphoneApplication.Companion.corePreferences +import org.linphone.core.Account +import org.linphone.core.AuthInfo +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.Factory +import org.linphone.core.RegistrationState +import org.linphone.core.tools.Log +import org.linphone.utils.Event + +class AssistantViewModel : ViewModel() { + companion object { + const val TAG = "[Assistant ViewModel]" + } + val username = MutableLiveData() + + val password = MutableLiveData() + + val loginEnabled = MediatorLiveData() + + val registrationInProgress = MutableLiveData() + + val accountLoggedInEvent = MutableLiveData>() + + private lateinit var newlyCreatedAuthInfo: AuthInfo + private lateinit var newlyCreatedAccount: Account + + private val coreListener = object : CoreListenerStub() { + override fun onAccountRegistrationStateChanged( + core: Core, + account: Account, + state: RegistrationState?, + message: String + ) { + // Core thread + if (account == newlyCreatedAccount) { + Log.i("$TAG Newly created account registration state is [$state] ($message)") + + if (state == RegistrationState.Ok) { + registrationInProgress.postValue(false) + core.removeListener(this) + + // Set new account as default + core.defaultAccount = newlyCreatedAccount + coreContext.contactsManager.updateLocalContacts() + accountLoggedInEvent.postValue(Event(true)) + } else if (state == RegistrationState.Failed) { + registrationInProgress.postValue(false) + core.removeListener(this) + + // TODO FIXME: show error + + Log.e("$TAG Account failed to REGISTER, removing it") + core.removeAuthInfo(newlyCreatedAuthInfo) + core.removeAccount(newlyCreatedAccount) + } + } + } + } + + init { + registrationInProgress.value = false + + loginEnabled.addSource(username) { + loginEnabled.value = isLoginButtonEnabled() + } + loginEnabled.addSource(password) { + loginEnabled.value = isLoginButtonEnabled() + } + } + + fun login() { + // UI thread + coreContext.postOnCoreThread { core -> + core.loadConfigFromXml(corePreferences.linphoneDefaultValuesPath) + + val user = username.value.orEmpty() + val domain = corePreferences.defaultDomain + + newlyCreatedAuthInfo = Factory.instance().createAuthInfo( + user, + null, + password.value.orEmpty(), + null, + null, + domain + ) + core.addAuthInfo(newlyCreatedAuthInfo) + + val accountParams = core.createAccountParams() + val identityAddress = Factory.instance().createAddress("sip:$user@$domain") + accountParams.identityAddress = identityAddress + newlyCreatedAccount = core.createAccount(accountParams) + + registrationInProgress.postValue(true) + core.addListener(coreListener) + core.addAccount(newlyCreatedAccount) + } + } + + private fun isLoginButtonEnabled(): Boolean { + return username.value.orEmpty().isNotEmpty() && password.value.orEmpty().isNotEmpty() + } +} diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt index e8b096e0e..4390fbcc1 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/NewContactFragment.kt @@ -61,8 +61,12 @@ class NewContactFragment : GenericFragment() { } viewModel.saveChangesEvent.observe(viewLifecycleOwner) { - it.consume { - goBack() // TODO FIXME : go to contact detail view + it.consume { ok -> + if (ok) { + goBack() // TODO FIXME : go to contact detail view + } else { + // TODO : show error + } } } } diff --git a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt index 3d2dc6168..7a1fa25c4 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/model/ContactAvatarModel.kt @@ -31,6 +31,8 @@ import org.linphone.utils.LinphoneUtils class ContactAvatarModel(val friend: Friend) { val id = friend.refKey + val avatar = MutableLiveData() + val initials = LinphoneUtils.getInitials(friend.name.orEmpty()) val presenceStatus = MutableLiveData() @@ -53,6 +55,7 @@ class ContactAvatarModel(val friend: Friend) { // Core thread name.postValue(friend.name) presenceStatus.postValue(friend.consolidatedPresence) + avatar.postValue(getAvatarUri()) friend.addListener(friendListener) @@ -64,8 +67,13 @@ class ContactAvatarModel(val friend: Friend) { friend.removeListener(friendListener) } - fun getAvatarUri(): Uri? { + private fun getAvatarUri(): Uri? { // Core thread + val picturePath = friend.photo + if (!picturePath.isNullOrEmpty()) { + return Uri.parse(picturePath) + } + val refKey = friend.refKey if (refKey != null) { val lookupUri = ContentUris.withAppendedId( diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt index a75e2165e..7077e33b6 100644 --- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt @@ -77,6 +77,7 @@ class ContactNewOrEditViewModel() : ViewModel() { friendFoundEvent.postValue(Event(true)) } else { Log.e("$TAG No friend found using ref key [$refKey]") + // TODO : generate unique ref key } } } diff --git a/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt b/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt new file mode 100644 index 000000000..281a931a8 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/model/AccountModel.kt @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2010-2023 Belledonne Communications SARL. + * + * This file is part of linphone-android + * (see https://www.linphone.org). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.linphone.ui.main.model + +import androidx.lifecycle.MutableLiveData +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Account +import org.linphone.core.AccountListenerStub +import org.linphone.core.Friend +import org.linphone.core.RegistrationState +import org.linphone.ui.main.contacts.model.ContactAvatarModel + +class AccountModel(private val account: Account) { + val friend: Friend? + + val contact = MutableLiveData() + + val registrationState = MutableLiveData() + + val isConnected = MutableLiveData() + + val inError = MutableLiveData() + + val isDefault = MutableLiveData() + + private val accountListener = object : AccountListenerStub() { + override fun onRegistrationStateChanged( + account: Account, + state: RegistrationState?, + message: String + ) { + updateRegistrationState() + } + } + + init { + // Core thread + account.addListener(accountListener) + isDefault.postValue(coreContext.core.defaultAccount == account) + + friend = coreContext.contactsManager.localFriends.find { + it.addresses.find { address -> + address.weakEqual(account.params.identityAddress!!) + } != null + } + + if (friend != null) { + contact.postValue(ContactAvatarModel(friend)) + } + + updateRegistrationState() + } + + fun destroy() { + // Core thread + account.removeListener(accountListener) + } + + fun setAsDefault() { + // UI thread + coreContext.postOnCoreThread { core -> + core.defaultAccount = account + isDefault.postValue(true) + } + } + + fun refreshRegister() { + // UI thread + coreContext.postOnCoreThread { core -> + core.refreshRegisters() + } + } + + private fun updateRegistrationState() { + // Core thread + val state = when (account.state) { + RegistrationState.None, RegistrationState.Cleared -> "Disabled" + RegistrationState.Progress -> "Connection..." + RegistrationState.Failed -> "Error" + RegistrationState.Ok -> "Connected" + RegistrationState.Refreshing -> "Refreshing" + else -> "${account.state}" + } + isConnected.postValue(account.state == RegistrationState.Ok) + inError.postValue(account.state == RegistrationState.Failed) + registrationState.postValue(state) + } +} diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt index 4feef1108..a3bf1f034 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/DrawerMenuViewModel.kt @@ -21,9 +21,16 @@ package org.linphone.ui.main.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.core.Account +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.RegistrationState +import org.linphone.ui.main.model.AccountModel import org.linphone.utils.Event class DrawerMenuViewModel : ViewModel() { + val accounts = MutableLiveData>() val startAssistantEvent: MutableLiveData> by lazy { MutableLiveData>() @@ -33,6 +40,33 @@ class DrawerMenuViewModel : ViewModel() { MutableLiveData>() } + private val coreListener = object : CoreListenerStub() { + override fun onAccountRegistrationStateChanged( + core: Core, + account: Account, + state: RegistrationState?, + message: String + ) { + // Core thread + computeAccountsList() + } + } + + init { + coreContext.postOnCoreThread { core -> + core.addListener(coreListener) + computeAccountsList() + } + } + + override fun onCleared() { + super.onCleared() + + coreContext.postOnCoreThread { core -> + core.removeListener(coreListener) + } + } + fun closeDrawerMenu() { // UI thread closeDrawerEvent.value = Event(true) @@ -42,4 +76,16 @@ class DrawerMenuViewModel : ViewModel() { // UI thread startAssistantEvent.value = Event(true) } + + private fun computeAccountsList() { + // Core thread + accounts.value.orEmpty().forEach(AccountModel::destroy) + + val list = arrayListOf() + for (account in coreContext.core.accountList) { + val model = AccountModel(account) + list.add(model) + } + accounts.postValue(list) + } } diff --git a/app/src/main/java/org/linphone/ui/main/viewmodel/TopBarViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewmodel/TopBarViewModel.kt index dfde83e8a..550cd8a30 100644 --- a/app/src/main/java/org/linphone/ui/main/viewmodel/TopBarViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewmodel/TopBarViewModel.kt @@ -21,11 +21,15 @@ package org.linphone.ui.main.viewmodel import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.ui.main.model.AccountModel import org.linphone.utils.Event class TopBarViewModel : ViewModel() { val title = MutableLiveData() + val account = MutableLiveData() + val searchBarVisible = MutableLiveData() val searchFilter = MutableLiveData() @@ -40,10 +44,21 @@ class TopBarViewModel : ViewModel() { init { searchBarVisible.value = false + + coreContext.postOnCoreThread { core -> + if (core.accountList.isNotEmpty()) { + val defaultAccount = core.defaultAccount ?: core.accountList.first() + account.postValue(AccountModel(defaultAccount)) + } + } } override fun onCleared() { super.onCleared() + + coreContext.postOnCoreThread { + account.value?.destroy() + } } fun openDrawerMenu() { diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 03155a714..c5be397b3 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -131,6 +131,7 @@ fun AppCompatTextView.setDrawableTint(color: Int) { @BindingAdapter("coilContact") fun loadContactPictureWithCoil2(imageView: ImageView, contact: ContactData?) { + // UI thread ! if (contact == null) { imageView.load(R.drawable.contact_avatar) } else { @@ -143,6 +144,7 @@ fun loadContactPictureWithCoil2(imageView: ImageView, contact: ContactData?) { @BindingAdapter("contactAvatar") fun AvatarView.loadContactPicture(contact: ContactAvatarModel?) { + // UI thread ! if (contact == null) { loadImage(R.drawable.contact_avatar) } else { @@ -152,7 +154,7 @@ fun AvatarView.loadContactPicture(contact: ContactAvatarModel?) { } indicatorEnabled = contact.presenceStatus.value != ConsolidatedPresence.Offline - val uri = contact.getAvatarUri() + val uri = contact.avatar.value loadImage( data = uri, onStart = { diff --git a/app/src/main/res/layout/account_list_cell.xml b/app/src/main/res/layout/account_list_cell.xml index 588af8c12..0a457edc2 100644 --- a/app/src/main/res/layout/account_list_cell.xml +++ b/app/src/main/res/layout/account_list_cell.xml @@ -8,18 +8,22 @@ - + type="org.linphone.ui.main.model.AccountModel" /> + + + + + - - - - + android:orientation="vertical" + entries="@{viewModel.accounts}" + layout="@{@layout/account_list_cell}"/> diff --git a/app/src/main/res/layout/top_search_bar.xml b/app/src/main/res/layout/top_search_bar.xml index 84474edfb..e41b677d8 100644 --- a/app/src/main/res/layout/top_search_bar.xml +++ b/app/src/main/res/layout/top_search_bar.xml @@ -40,6 +40,7 @@ android:layout_width="@dimen/avatar_list_cell_size" android:layout_height="@dimen/avatar_list_cell_size" android:layout_marginStart="15dp" + contactAvatar="@{viewModel.account.contact}" app:avatarViewBorderColor="@color/trusted_blue" app:avatarViewBorderWidth="2dp" app:avatarViewIndicatorSizeCriteria="3" @@ -52,7 +53,6 @@ app:avatarViewInitialsTextStyle="bold" app:avatarViewPlaceholder="@drawable/contact_avatar" app:avatarViewShape="circle" - app:avatarViewInitials="SB" app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="@id/title" />