Started account login & proper display of accounts

This commit is contained in:
Sylvain Berfini 2023-08-17 10:41:15 +02:00
parent faa4309ece
commit 5e1c681a8d
15 changed files with 402 additions and 23 deletions

View file

@ -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<Friend>()
private val listeners = arrayListOf<ContactsListener>()
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() {

View file

@ -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) {

View file

@ -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()
}
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String>()
val password = MutableLiveData<String>()
val loginEnabled = MediatorLiveData<Boolean>()
val registrationInProgress = MutableLiveData<Boolean>()
val accountLoggedInEvent = MutableLiveData<Event<Boolean>>()
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()
}
}

View file

@ -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
}
}
}
}

View file

@ -31,6 +31,8 @@ import org.linphone.utils.LinphoneUtils
class ContactAvatarModel(val friend: Friend) {
val id = friend.refKey
val avatar = MutableLiveData<Uri>()
val initials = LinphoneUtils.getInitials(friend.name.orEmpty())
val presenceStatus = MutableLiveData<ConsolidatedPresence>()
@ -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(

View file

@ -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
}
}
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ContactAvatarModel>()
val registrationState = MutableLiveData<String>()
val isConnected = MutableLiveData<Boolean>()
val inError = MutableLiveData<Boolean>()
val isDefault = MutableLiveData<Boolean>()
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)
}
}

View file

@ -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<ArrayList<AccountModel>>()
val startAssistantEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
@ -33,6 +40,33 @@ class DrawerMenuViewModel : ViewModel() {
MutableLiveData<Event<Boolean>>()
}
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<AccountModel>()
for (account in coreContext.core.accountList) {
val model = AccountModel(account)
list.add(model)
}
accounts.postValue(list)
}
}

View file

@ -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<String>()
val account = MutableLiveData<AccountModel>()
val searchBarVisible = MutableLiveData<Boolean>()
val searchFilter = MutableLiveData<String>()
@ -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() {

View file

@ -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 = {

View file

@ -8,18 +8,22 @@
<import type="android.graphics.Typeface" />
<variable
name="model"
type="org.linphone.ui.main.contacts.model.ContactAvatarModel" />
<variable
name="onClickListener"
type="View.OnClickListener" />
type="org.linphone.ui.main.model.AccountModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{onClickListener}"
android:onClick="@{() -> model.setAsDefault()}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/shape_conversation_cell_background">
<androidx.constraintlayout.widget.Barrier
android:id="@+id/left_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="right"
app:constraint_referenced_ids="name, register_status" />
<io.getstream.avatarview.AvatarView
android:id="@+id/avatar"
android:layout_width="@dimen/avatar_list_cell_size"
@ -28,8 +32,7 @@
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
contactAvatar="@{model}"
app:avatarViewInitials="JD"
contactAvatar="@{model.contact}"
app:avatarViewPlaceholder="@drawable/contact_avatar"
app:avatarViewInitialsBackgroundColor="@color/blue_outgoing_message"
app:avatarViewInitialsTextColor="@color/gray_9"
@ -53,7 +56,7 @@
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:text="John Doe"
android:text="@{model.contact.name, default=`John Doe`}"
android:textSize="14sp"
android:textColor="@color/gray_8"
android:layout_marginStart="10dp"
@ -62,6 +65,7 @@
app:layout_constraintBottom_toTopOf="@id/register_status"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> model.refreshRegister()}"
style="@style/default_text_style_300"
android:id="@+id/register_status"
android:layout_width="wrap_content"
@ -73,13 +77,29 @@
android:paddingBottom="4dp"
android:background="@drawable/shape_chip_gray_background"
android:gravity="center"
android:text="Connected"
android:textColor="@color/green_online"
android:text="@{model.registrationState, default=`Connected`}"
android:textColor="@{model.isConnected ? @color/green_online : model.inError ? @color/red_danger : @color/gray_1, default=@color/green_online}"
android:textSize="12sp"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_constraintBottom_toTopOf="@id/separator"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style_300"
android:visibility="@{model.isDefault ? View.VISIBLE : View.GONE}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Default"
android:textSize="12sp"
android:gravity="center"
android:drawableStart="@drawable/check"
android:drawablePadding="3dp"
app:drawableTint="@color/primary_color"
app:layout_constraintStart_toEndOf="@id/left_barrier"
app:layout_constraintEnd_toStartOf="@id/menu"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:id="@+id/menu"
android:layout_width="wrap_content"

View file

@ -8,6 +8,9 @@
<variable
name="registerClickListener"
type="View.OnClickListener" />
<variable
name="viewModel"
type="org.linphone.ui.assistant.viewmodel.AssistantViewModel" />
</data>
<ScrollView
@ -55,7 +58,7 @@
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="John Doe"
android:text="@={viewModel.username, default=`johndoe`}"
android:textSize="14sp"
android:textColor="@color/gray_9"
android:background="@drawable/shape_edit_text_background"
@ -86,7 +89,7 @@
android:layout_marginEnd="16dp"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:text="John Doe"
android:text="@={viewModel.password, default=`johndoe`}"
android:textSize="14sp"
android:textColor="@color/gray_9"
android:background="@drawable/shape_edit_text_background"
@ -98,6 +101,8 @@
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatTextView
android:onClick="@{() -> viewModel.login()}"
android:enabled="@{viewModel.loginEnabled &amp;&amp; !viewModel.registrationInProgress, default=false}"
style="@style/default_text_style_600"
android:id="@+id/login"
android:layout_width="0dp"

View file

@ -60,12 +60,9 @@
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:orientation="vertical">
<include
layout="@layout/account_list_cell" />
</LinearLayout>
android:orientation="vertical"
entries="@{viewModel.accounts}"
layout="@{@layout/account_list_cell}"/>
</ScrollView>

View file

@ -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" />