Reworked presence badge

This commit is contained in:
Sylvain Berfini 2023-08-17 11:44:19 +02:00
parent 5e1c681a8d
commit e5bbe3a553
22 changed files with 220 additions and 51 deletions

View file

@ -21,6 +21,8 @@ use_cpim=1
zrtp_key_agreements_suites=MS_ZRTP_KEY_AGREEMENT_K255_KYB512
chat_messages_aggregation_delay=1000
chat_messages_aggregation=1
update_presence_model_timestamp_before_publish_expires_refresh=1
rls_uri=sips:rls@sip.linphone.org
[sound]
#remove this property for any application that is not Linphone public version itself

View file

@ -21,16 +21,45 @@ package org.linphone.contacts
import androidx.loader.app.LoaderManager
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.core.Core
import org.linphone.core.CoreListenerStub
import org.linphone.core.Friend
import org.linphone.core.FriendList
import org.linphone.core.FriendListListenerStub
import org.linphone.core.tools.Log
import org.linphone.ui.main.MainActivity
import org.linphone.utils.LinphoneUtils
class ContactsManager {
companion object {
const val TAG = "[Contacts Manager]"
}
val localFriends = arrayListOf<Friend>()
private val listeners = arrayListOf<ContactsListener>()
private val friendListListener: FriendListListenerStub = object : FriendListListenerStub() {
override fun onPresenceReceived(list: FriendList, friends: Array<Friend>) {
// Core thread
Log.i("$TAG Presence received")
for (listener in listeners) {
listener.onContactsLoaded()
}
}
}
private val coreListener: CoreListenerStub = object : CoreListenerStub() {
override fun onFriendListCreated(core: Core, friendList: FriendList) {
// Core thread
friendList.addListener(friendListListener)
}
override fun onFriendListRemoved(core: Core, friendList: FriendList) {
// Core thread
friendList.removeListener(friendListListener)
}
}
fun loadContacts(activity: MainActivity) {
// UI thread
val manager = LoaderManager.getInstance(activity)
@ -73,7 +102,7 @@ class ContactsManager {
fun updateLocalContacts() {
// Core thread
Log.i("[Contacts Manager] Updating local contact(s)")
Log.i("$TAG Updating local contact(s)")
localFriends.clear()
for (account in coreContext.core.accountList) {
@ -84,7 +113,7 @@ class ContactsManager {
friend.address = address
Log.i(
"[Contacts Manager] Local contact created for account [${address.asString()}] and picture [${friend.photo}]"
"$TAG Local contact created for account [${address.asString()}] and picture [${friend.photo}]"
)
localFriends.add(friend)
}
@ -92,11 +121,22 @@ class ContactsManager {
fun onCoreStarted() {
// Core thread
val core = coreContext.core
core.addListener(coreListener)
for (list in core.friendsLists) {
list.addListener(friendListListener)
}
updateLocalContacts()
}
fun onCoreStopped() {
// Core thread
val core = coreContext.core
core.removeListener(coreListener)
for (list in core.friendsLists) {
list.removeListener(friendListListener)
}
}
}

View file

@ -32,11 +32,12 @@ import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.contacts.ContactsManager
import org.linphone.core.tools.Log
import org.linphone.ui.voip.VoipActivity
import org.linphone.utils.LinphoneUtils
class CoreContext(val context: Context) : HandlerThread("Core Thread") {
lateinit var core: Core
lateinit var emojiCompat: EmojiCompat
val emojiCompat: EmojiCompat
val contactsManager = ContactsManager()
@ -96,6 +97,7 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
)
computeUserAgent()
core.start()
contactsManager.onCoreStarted()
@ -213,9 +215,8 @@ class CoreContext(val context: Context) : HandlerThread("Core Thread") {
}
private fun computeUserAgent() {
// TODO FIXME
val deviceName: String = "Linphone6"
val appName: String = "Linphone Android"
val deviceName = LinphoneUtils.getDeviceName(context)
val appName = context.getString(org.linphone.R.string.app_name)
val androidVersion = BuildConfig.VERSION_NAME
val userAgent = "$appName/$androidVersion ($deviceName) LinphoneSDK"
val sdkVersion = context.getString(org.linphone.core.R.string.linphone_sdk_version)

View file

@ -122,6 +122,7 @@ private class ContactDiffCallback : DiffUtil.ItemCallback<ContactAvatarModel>()
}
override fun areContentsTheSame(oldItem: ContactAvatarModel, newItem: ContactAvatarModel): Boolean {
return oldItem.showFirstLetter.value == newItem.showFirstLetter.value
return oldItem.showFirstLetter.value == newItem.showFirstLetter.value &&
oldItem.presenceStatus.value == newItem.presenceStatus.value
}
}

View file

@ -26,9 +26,14 @@ import androidx.lifecycle.MutableLiveData
import org.linphone.core.ConsolidatedPresence
import org.linphone.core.Friend
import org.linphone.core.FriendListenerStub
import org.linphone.core.tools.Log
import org.linphone.utils.LinphoneUtils
class ContactAvatarModel(val friend: Friend) {
companion object {
const val TAG = "[Contact Avatar Model]"
}
val id = friend.refKey
val avatar = MutableLiveData<Uri>()
@ -47,19 +52,21 @@ class ContactAvatarModel(val friend: Friend) {
private val friendListener = object : FriendListenerStub() {
override fun onPresenceReceived(fr: Friend) {
Log.d(
"$TAG Presence received for friend [${fr.name}]: [${friend.consolidatedPresence}]"
)
presenceStatus.postValue(fr.consolidatedPresence)
}
}
init {
// Core thread
name.postValue(friend.name)
presenceStatus.postValue(friend.consolidatedPresence)
avatar.postValue(getAvatarUri())
friend.addListener(friendListener)
presenceStatus.postValue(ConsolidatedPresence.Offline)
name.postValue(friend.name)
presenceStatus.postValue(friend.consolidatedPresence)
Log.d("$TAG Friend [${friend.name}] presence status is [${friend.consolidatedPresence}]")
avatar.postValue(getAvatarUri())
}
fun destroy() {

View file

@ -32,6 +32,10 @@ import org.linphone.core.tools.Log
import org.linphone.ui.main.contacts.model.ContactAvatarModel
class ContactsListViewModel : ViewModel() {
companion object {
const val TAG = "[Contacts List ViewModel]"
}
val contactsList = MutableLiveData<ArrayList<ContactAvatarModel>>()
val favourites = MutableLiveData<ArrayList<ContactAvatarModel>>()
@ -48,7 +52,7 @@ class ContactsListViewModel : ViewModel() {
private val magicSearchListener = object : MagicSearchListenerStub() {
override fun onSearchResultsReceived(magicSearch: MagicSearch) {
// Core thread
Log.i("[Contacts] Magic search contacts available")
Log.i("$TAG Magic search contacts available")
processMagicSearchResults(magicSearch.lastSearch)
}
}
@ -56,6 +60,7 @@ class ContactsListViewModel : ViewModel() {
private val contactsListener = object : ContactsListener {
override fun onContactsLoaded() {
// Core thread
Log.i("$TAG Contacts have been (re)loaded, updating list")
applyFilter(
currentFilter,
"",
@ -92,7 +97,7 @@ class ContactsListViewModel : ViewModel() {
fun processMagicSearchResults(results: Array<SearchResult>) {
// Core thread
Log.i("[Contacts List] Processing ${results.size} results")
Log.i("$TAG Processing ${results.size} results")
contactsList.value.orEmpty().forEach(ContactAvatarModel::destroy)
val list = arrayListOf<ContactAvatarModel>()
@ -107,7 +112,7 @@ class ContactsListViewModel : ViewModel() {
currentLetter = friend.name?.get(0).toString()
ContactAvatarModel(friend)
} else {
Log.w("[Contacts] SearchResult [$result] has no Friend!")
Log.w("$TAG SearchResult [$result] has no Friend!")
val fakeFriend =
createFriendFromSearchResult(result)
currentLetter = fakeFriend.name?.get(0).toString()
@ -129,7 +134,7 @@ class ContactsListViewModel : ViewModel() {
favourites.postValue(favouritesList)
contactsList.postValue(list)
Log.i("[Contacts] Processed ${results.size} results")
Log.i("$TAG Processed ${results.size} results")
}
fun applyFilter(filter: String) {
@ -163,7 +168,7 @@ class ContactsListViewModel : ViewModel() {
previousFilter = filter
Log.i(
"[Contacts] Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]"
"$TAG Asking Magic search for contacts matching filter [$filter], domain [$domain] and in sources [$sources]"
)
magicSearch.getContactsListAsync(
filter,

View file

@ -81,6 +81,10 @@ class AccountModel(private val account: Account) {
}
}
fun openMenu() {
// UI thread
}
fun refreshRegister() {
// UI thread
coreContext.postOnCoreThread { core ->

View file

@ -131,7 +131,7 @@ fun AppCompatTextView.setDrawableTint(color: Int) {
@BindingAdapter("coilContact")
fun loadContactPictureWithCoil2(imageView: ImageView, contact: ContactData?) {
// UI thread !
// UI thread
if (contact == null) {
imageView.load(R.drawable.contact_avatar)
} else {
@ -142,18 +142,24 @@ fun loadContactPictureWithCoil2(imageView: ImageView, contact: ContactData?) {
}
}
@BindingAdapter("presenceIcon")
fun ImageView.setPresenceIcon(presence: ConsolidatedPresence?) {
// UI thread
val icon = when (presence) {
ConsolidatedPresence.Online -> R.drawable.led_online
ConsolidatedPresence.DoNotDisturb -> R.drawable.led_do_not_disturb
ConsolidatedPresence.Busy -> R.drawable.led_away
else -> R.drawable.led_not_registered
}
setImageResource(icon)
}
@BindingAdapter("contactAvatar")
fun AvatarView.loadContactPicture(contact: ContactAvatarModel?) {
// UI thread !
// UI thread
if (contact == null) {
loadImage(R.drawable.contact_avatar)
} else {
indicatorColor = when (contact.presenceStatus.value) {
ConsolidatedPresence.Online -> R.color.green_online
else -> R.color.blue_outgoing_message
}
indicatorEnabled = contact.presenceStatus.value != ConsolidatedPresence.Offline
val uri = contact.avatar.value
loadImage(
data = uri,

View file

@ -19,6 +19,10 @@
*/
package org.linphone.utils
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.os.Build
import android.provider.Settings
import androidx.annotation.IntegerRes
import androidx.emoji2.text.EmojiCompat
import java.util.Locale
@ -137,5 +141,26 @@ class LinphoneUtils {
fun getChatRoomId(chatRoom: ChatRoom): String {
return getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
}
fun getDeviceName(context: Context): String {
var name = Settings.Global.getString(
context.contentResolver,
Settings.Global.DEVICE_NAME
)
if (name == null) {
val adapter = BluetoothAdapter.getDefaultAdapter()
name = adapter?.name
}
if (name == null) {
name = Settings.Secure.getString(
context.contentResolver,
"bluetooth_name"
)
}
if (name == null) {
name = Build.MANUFACTURER + " " + Build.MODEL
}
return name
}
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/orange_away"/>
<size android:width="15dp" android:height="15dp"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/white"/>
<size android:width="20dp" android:height="20dp"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/red_danger"/>
<size android:width="15dp" android:height="15dp"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/gray_offline"/>
<size android:width="15dp" android:height="15dp"/>
</shape>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/green_online"/>
<size android:width="15dp" android:height="15dp"/>
</shape>

View file

@ -101,13 +101,16 @@
app:layout_constraintBottom_toBottomOf="parent"/>
<ImageView
android:onClick="@{() -> model.openMenu()}"
android:id="@+id/menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/dot_menu"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:paddingStart="10dp"
android:paddingEnd="10dp"
app:tint="@color/gray_1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/name"

View file

@ -71,11 +71,6 @@
android:adjustViewBounds="true"
contactAvatar="@{viewModel.callLogModel.avatarModel}"
app:avatarViewBorderWidth="0dp"
app:avatarViewIndicatorBorderColor="@color/white"
app:avatarViewIndicatorBorderSizeCriteria="8"
app:avatarViewIndicatorEnabled="true"
app:avatarViewIndicatorPosition="bottomRight"
app:avatarViewIndicatorSizeCriteria="7"
app:avatarViewInitialsBackgroundColor="@color/blue_outgoing_message"
app:avatarViewInitialsTextColor="@color/gray_9"
app:avatarViewInitialsTextSize="21sp"
@ -86,6 +81,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/presence_badge"
android:layout_width="@dimen/avatar_presence_badge_big_size"
android:layout_height="@dimen/avatar_presence_badge_big_size"
android:layout_marginEnd="@dimen/avatar_presence_badge_big_end_margin"
android:background="@drawable/led_background"
android:padding="@dimen/avatar_presence_badge_big_padding"
app:presenceIcon="@{viewModel.callLogModel.avatarModel.presenceStatus}"
android:visibility="@{viewModel.callLogModel.avatarModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/name"
@ -118,7 +125,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.callLogModel.avatarModel.presenceStatus == ConsolidatedPresence.Online ? `En ligne` : `Absent`, default=`En ligne`}"
android:textColor="@{viewModel.callLogModel.avatarModel.presenceStatus == ConsolidatedPresence.Online ? @color/green_online : @color/green_online, default=@color/green_online}"
android:textColor="@{viewModel.callLogModel.avatarModel.presenceStatus == ConsolidatedPresence.Online ? @color/green_online : @color/orange_away, default=@color/green_online}"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -6,6 +6,7 @@
<data>
<import type="android.view.View" />
<import type="android.graphics.Typeface" />
<import type="org.linphone.core.ConsolidatedPresence" />
<variable
name="model"
type="org.linphone.ui.main.calls.model.CallLogModel" />
@ -48,6 +49,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/presence_badge"
android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size"
android:layout_marginEnd="@dimen/avatar_presence_badge_end_margin"
android:background="@drawable/led_background"
android:padding="@dimen/avatar_presence_badge_padding"
app:presenceIcon="@{model.avatarModel.presenceStatus}"
android:visibility="@{model.avatarModel.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/name"

View file

@ -5,6 +5,7 @@
<data>
<import type="android.view.View" />
<import type="org.linphone.core.ConsolidatedPresence" />
<variable
name="model"
type="org.linphone.ui.main.contacts.model.ContactAvatarModel" />
@ -37,16 +38,22 @@
app:avatarViewInitialsTextSize="16sp"
app:avatarViewInitialsTextStyle="bold"
app:avatarViewShape="circle"
app:avatarViewBorderWidth="0dp"
app:avatarViewIndicatorEnabled="true"
app:avatarViewIndicatorBorderColor="@color/white"
app:avatarViewIndicatorSizeCriteria="7"
app:avatarViewIndicatorBorderSizeCriteria="8"
app:avatarViewIndicatorPosition="bottomRight"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/presence_badge"
android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size"
android:layout_marginEnd="@dimen/avatar_presence_badge_end_margin"
android:background="@drawable/led_background"
android:padding="@dimen/avatar_presence_badge_padding"
app:presenceIcon="@{model.presenceStatus}"
android:visibility="@{model.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/name"

View file

@ -89,11 +89,6 @@
android:adjustViewBounds="true"
contactAvatar="@{viewModel.contact}"
app:avatarViewBorderWidth="0dp"
app:avatarViewIndicatorBorderColor="@color/white"
app:avatarViewIndicatorBorderSizeCriteria="8"
app:avatarViewIndicatorEnabled="true"
app:avatarViewIndicatorPosition="bottomRight"
app:avatarViewIndicatorSizeCriteria="7"
app:avatarViewInitialsBackgroundColor="@color/blue_outgoing_message"
app:avatarViewInitialsTextColor="@color/gray_9"
app:avatarViewInitialsTextSize="21sp"
@ -104,6 +99,18 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/presence_badge"
android:layout_width="@dimen/avatar_presence_badge_big_size"
android:layout_height="@dimen/avatar_presence_badge_big_size"
android:layout_marginEnd="@dimen/avatar_presence_badge_big_end_margin"
android:background="@drawable/led_background"
android:padding="@dimen/avatar_presence_badge_big_padding"
app:presenceIcon="@{viewModel.contact.presenceStatus}"
android:visibility="@{viewModel.contact.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/name"
@ -124,7 +131,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.contact.presenceStatus == ConsolidatedPresence.Online ? `En ligne` : `Absent`, default=`En ligne`}"
android:textColor="@{viewModel.contact.presenceStatus == ConsolidatedPresence.Online ? @color/green_online : @color/green_online, default=@color/green_online}"
android:textColor="@{viewModel.contact.presenceStatus == ConsolidatedPresence.Online ? @color/green_online : @color/orange_away, default=@color/green_online}"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"

View file

@ -6,6 +6,7 @@
<data>
<import type="android.view.View" />
<import type="android.graphics.Typeface" />
<import type="org.linphone.core.ConsolidatedPresence" />
<variable
name="model"
type="org.linphone.ui.main.contacts.model.ContactAvatarModel" />
@ -47,22 +48,29 @@
android:layout_marginBottom="5dp"
android:adjustViewBounds="true"
contactAvatar="@{model}"
app:avatarViewInitials="SB"
app:avatarViewPlaceholder="@drawable/contact_avatar"
app:avatarViewInitialsBackgroundColor="@color/blue_outgoing_message"
app:avatarViewInitialsTextColor="@color/gray_9"
app:avatarViewInitialsTextSize="16sp"
app:avatarViewInitialsTextStyle="bold"
app:avatarViewShape="circle"
app:avatarViewBorderWidth="0dp"
app:avatarViewIndicatorEnabled="true"
app:avatarViewIndicatorBorderColor="@color/white"
app:avatarViewIndicatorSizeCriteria="7"
app:avatarViewIndicatorBorderSizeCriteria="8"
app:avatarViewIndicatorPosition="bottomRight"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/header"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/presence_badge"
android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size"
android:layout_marginEnd="@dimen/avatar_presence_badge_end_margin"
android:background="@drawable/led_background"
android:padding="@dimen/avatar_presence_badge_padding"
app:presenceIcon="@{model.presenceStatus}"
android:visibility="@{model.presenceStatus == ConsolidatedPresence.Offline ? View.GONE : View.VISIBLE}"
app:layout_constraintEnd_toEndOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style"
android:id="@+id/name"

View file

@ -12,6 +12,8 @@
<color name="blue_outgoing_message">#DFECF2</color>
<color name="gray_incoming_message">#F4F4F7</color>
<color name="trusted_blue">#4AA8FF</color>
<color name="orange_away">#FFA645</color>
<color name="gray_offline">#E1E1E1</color>
<color name="warning_orange_background">#FFEACB</color>
<color name="warning_orange_pressed_background">#FFB266</color>
<color name="dialog_background">#22334D</color>

View file

@ -10,6 +10,12 @@
<dimen name="avatar_favorite_list_cell_size">50dp</dimen>
<dimen name="avatar_big_size">100dp</dimen>
<dimen name="avatar_in_call_size">120dp</dimen>
<dimen name="avatar_presence_badge_size">12dp</dimen>
<dimen name="avatar_presence_badge_padding">2dp</dimen>
<dimen name="avatar_presence_badge_end_margin">3dp</dimen>
<dimen name="avatar_presence_badge_big_size">22dp</dimen>
<dimen name="avatar_presence_badge_big_padding">3dp</dimen>
<dimen name="avatar_presence_badge_big_end_margin">5dp</dimen>
<dimen name="top_search_bar_height">55dp</dimen>