diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1cb580d2a..0cfbf1359 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -121,6 +121,12 @@
android:launchMode="singleTop"
android:noHistory="true" />
+
+
.
+ */
+package org.linphone.activities.chat_bubble
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.R
+import org.linphone.activities.GenericActivity
+import org.linphone.activities.main.MainActivity
+import org.linphone.activities.main.chat.adapters.ChatMessagesListAdapter
+import org.linphone.activities.main.chat.viewmodels.*
+import org.linphone.activities.main.viewmodels.ListTopBarViewModel
+import org.linphone.core.ChatRoom
+import org.linphone.core.Factory
+import org.linphone.core.tools.Log
+import org.linphone.databinding.ChatBubbleActivityBinding
+
+class ChatBubbleActivity : GenericActivity() {
+ private lateinit var binding: ChatBubbleActivityBinding
+ private lateinit var viewModel: ChatRoomViewModel
+ private lateinit var listViewModel: ChatMessagesListViewModel
+ private lateinit var chatSendingViewModel: ChatMessageSendingViewModel
+ private lateinit var adapter: ChatMessagesListAdapter
+
+ private val observer = object : RecyclerView.AdapterDataObserver() {
+ override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
+ if (positionStart == adapter.itemCount - 1) {
+ adapter.notifyItemChanged(positionStart - 1) // For grouping purposes
+ scrollToBottom()
+ }
+ }
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ binding = DataBindingUtil.setContentView(this, R.layout.chat_bubble_activity)
+ binding.lifecycleOwner = this
+
+ val localSipUri = intent.getStringExtra("LocalSipUri")
+ val remoteSipUri = intent.getStringExtra("RemoteSipUri")
+ var chatRoom: ChatRoom? = null
+
+ if (localSipUri != null && remoteSipUri != null) {
+ Log.i("[Chat Bubble] Found local [$localSipUri] & remote [$remoteSipUri] addresses in arguments")
+ val localAddress = Factory.instance().createAddress(localSipUri)
+ val remoteSipAddress = Factory.instance().createAddress(remoteSipUri)
+ chatRoom = coreContext.core.searchChatRoom(
+ null, localAddress, remoteSipAddress, arrayOfNulls(
+ 0
+ )
+ )
+ }
+
+ chatRoom ?: return
+ chatRoom.markAsRead()
+
+ viewModel = ViewModelProvider(
+ this,
+ ChatRoomViewModelFactory(chatRoom)
+ )[ChatRoomViewModel::class.java]
+ binding.viewModel = viewModel
+
+ listViewModel = ViewModelProvider(
+ this,
+ ChatMessagesListViewModelFactory(chatRoom)
+ )[ChatMessagesListViewModel::class.java]
+
+ chatSendingViewModel = ViewModelProvider(
+ this,
+ ChatMessageSendingViewModelFactory(chatRoom)
+ )[ChatMessageSendingViewModel::class.java]
+ binding.chatSendingViewModel = chatSendingViewModel
+
+ val listSelectionViewModel = ViewModelProvider(this).get(ListTopBarViewModel::class.java)
+ adapter = ChatMessagesListAdapter(listSelectionViewModel, this)
+ // SubmitList is done on a background thread
+ // We need this adapter data observer to know when to scroll
+ binding.chatMessagesList.adapter = adapter
+ adapter.registerAdapterDataObserver(observer)
+
+ // Disable context menu on each message
+ adapter.disableContextMenu()
+
+ val layoutManager = LinearLayoutManager(this)
+ layoutManager.stackFromEnd = true
+ binding.chatMessagesList.layoutManager = layoutManager
+
+ listViewModel.events.observe(this, { events ->
+ adapter.submitList(events)
+ })
+
+ chatSendingViewModel.textToSend.observe(this, {
+ chatSendingViewModel.onTextToSendChanged(it)
+ })
+
+ binding.setOpenAppClickListener {
+ val intent = Intent(this, MainActivity::class.java)
+ intent.putExtra("RemoteSipUri", remoteSipUri)
+ intent.putExtra("LocalSipUri", localSipUri)
+ intent.putExtra("Chat", true)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK and Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ startActivity(intent)
+ }
+
+ binding.setCloseBubbleClickListener {
+ val notificationId = coreContext.notificationsManager.getChatNotificationIdForSipUri(viewModel.chatRoom.peerAddress.asStringUriOnly())
+ coreContext.notificationsManager.cancel(notificationId)
+ }
+
+ binding.setSendMessageClickListener {
+ chatSendingViewModel.sendMessage()
+ binding.message.text?.clear()
+ }
+ }
+
+ private fun scrollToBottom() {
+ if (adapter.itemCount > 0) {
+ binding.chatMessagesList.scrollToPosition(adapter.itemCount - 1)
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
index 9405b74ac..85481a3d0 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt
@@ -83,6 +83,8 @@ class ChatMessagesListAdapter(
}
}
+ private var contextMenuDisabled: Boolean = false
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
EventLog.Type.ConferenceChatMessage.toInt() -> createChatMessageViewHolder(parent)
@@ -119,6 +121,10 @@ class ChatMessagesListAdapter(
return eventLog.type.toInt()
}
+ fun disableContextMenu() {
+ contextMenuDisabled = true
+ }
+
inner class ChatMessageViewHolder(
private val binding: ChatMessageListCellBinding
) : RecyclerView.ViewHolder(binding.root), PopupMenu.OnMenuItemClickListener {
@@ -176,6 +182,8 @@ class ChatMessagesListAdapter(
executePendingBindings()
+ if (contextMenuDisabled) return
+
setContextMenuClickListener {
val popup = PopupMenu(root.context, background)
popup.setOnMenuItemClickListener(this@ChatMessageViewHolder)
diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
index dbd28774d..8886c54eb 100644
--- a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
+++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt
@@ -72,8 +72,6 @@ class DetailChatRoomFragment : MasterFragment {
if (bm == null) IconCompat.createWithResource(
coreContext.context,
R.drawable.avatar
- ) else IconCompat.createWithBitmap(bm)
+ ) else IconCompat.createWithAdaptiveBitmap(bm)
if (icon != null) {
personBuilder.setIcon(icon)
}
diff --git a/app/src/main/java/org/linphone/contact/NativeContact.kt b/app/src/main/java/org/linphone/contact/NativeContact.kt
index 4788a6b8e..d26a5ba82 100644
--- a/app/src/main/java/org/linphone/contact/NativeContact.kt
+++ b/app/src/main/java/org/linphone/contact/NativeContact.kt
@@ -71,7 +71,7 @@ class NativeContact(val nativeId: String, private val lookupKey: String? = null)
if (bm == null) IconCompat.createWithResource(
coreContext.context,
R.drawable.avatar
- ) else IconCompat.createWithBitmap(bm)
+ ) else IconCompat.createWithAdaptiveBitmap(bm)
if (icon != null) {
personBuilder.setIcon(icon)
}
diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt
index 0be984f69..ed526bf4d 100644
--- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt
+++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt
@@ -42,6 +42,7 @@ import org.linphone.R
import org.linphone.activities.call.CallActivity
import org.linphone.activities.call.IncomingCallActivity
import org.linphone.activities.call.OutgoingCallActivity
+import org.linphone.activities.chat_bubble.ChatBubbleActivity
import org.linphone.activities.main.MainActivity
import org.linphone.compatibility.Compatibility
import org.linphone.contact.Contact
@@ -68,7 +69,8 @@ private class NotifiableMessage(
val time: Long,
val senderAvatar: Bitmap? = null,
var filePath: Uri? = null,
- var fileMime: String? = null
+ var fileMime: String? = null,
+ val isOutgoing: Boolean = false
)
class NotificationsManager(private val context: Context) {
@@ -149,6 +151,10 @@ class NotificationsManager(private val context: Context) {
return
}
+ if (corePreferences.chatRoomShortcuts) {
+ Log.i("[Notifications Manager] Ensure chat room shortcut exists for bubble notification")
+ Compatibility.createShortcutsToChatRooms(context)
+ }
displayIncomingChatNotification(room, message)
}
}
@@ -240,6 +246,14 @@ class NotificationsManager(private val context: Context) {
return null
}
+ fun getChatNotificationIdForSipUri(sipUri: String): Int {
+ val notifiable: Notifiable? = chatNotificationsMap[sipUri]
+ if (notifiable != null) {
+ return notifiable.notificationId
+ }
+ return 0
+ }
+
/* Service related */
fun startForeground() {
@@ -529,8 +543,13 @@ class NotificationsManager(private val context: Context) {
.setArguments(args)
.createPendingIntent()
+ val target = Intent(context, ChatBubbleActivity::class.java)
+ target.putExtra("RemoteSipUri", peerAddress)
+ target.putExtra("LocalSipUri", localAddress)
+ val bubbleIntent = PendingIntent.getActivity(context, notifiable.notificationId, target, PendingIntent.FLAG_UPDATE_CURRENT)
+
val id = LinphoneUtils.getChatRoomId(localAddress, peerAddress)
- val notification = createMessageNotification(notifiable, pendingIntent, id)
+ val notification = createMessageNotification(notifiable, pendingIntent, bubbleIntent, id)
notify(notifiable.notificationId, notification)
}
@@ -548,7 +567,7 @@ class NotificationsManager(private val context: Context) {
text += content.name
}
}
- val notifiableMessage = NotifiableMessage(text, contact, displayName, message.time, senderAvatar = roundPicture)
+ val notifiableMessage = NotifiableMessage(text, contact, displayName, message.time, senderAvatar = roundPicture, isOutgoing = message.isOutgoing)
notifiable.messages.add(notifiableMessage)
for (content in message.contents) {
@@ -601,7 +620,8 @@ class NotificationsManager(private val context: Context) {
message.textContent.orEmpty(),
null,
notifiable.myself ?: LinphoneUtils.getDisplayName(message.fromAddress),
- System.currentTimeMillis()
+ System.currentTimeMillis(),
+ isOutgoing = true
)
notifiable.messages.add(reply)
@@ -623,12 +643,14 @@ class NotificationsManager(private val context: Context) {
private fun createMessageNotification(
notifiable: Notifiable,
pendingIntent: PendingIntent,
+ bubbleIntent: PendingIntent,
id: String
): Notification {
val me = Person.Builder().setName(notifiable.myself).build()
val style = NotificationCompat.MessagingStyle(me)
val largeIcon: Bitmap? = notifiable.messages.last().senderAvatar
+ var lastPerson: Person? = null
for (message in notifiable.messages) {
val contact = message.contact
val person = if (contact != null) {
@@ -637,7 +659,7 @@ class NotificationsManager(private val context: Context) {
val builder = Person.Builder().setName(message.sender)
val userIcon =
if (message.senderAvatar != null) {
- IconCompat.createWithBitmap(message.senderAvatar)
+ IconCompat.createWithAdaptiveBitmap(message.senderAvatar)
} else {
IconCompat.createWithResource(context, R.drawable.avatar)
}
@@ -645,6 +667,11 @@ class NotificationsManager(private val context: Context) {
builder.build()
}
+ // We don't want to see our own avatar
+ if (!message.isOutgoing) {
+ lastPerson = person
+ }
+
val msg = if (!corePreferences.hideChatMessageContentInNotification) {
NotificationCompat.MessagingStyle.Message(message.message, message.time, person)
} else {
@@ -663,6 +690,10 @@ class NotificationsManager(private val context: Context) {
}
style.isGroupConversation = notifiable.isGroup
+ val icon = lastPerson?.icon ?: IconCompat.createWithResource(context, R.drawable.avatar)
+ val bubble = NotificationCompat.BubbleMetadata.Builder(bubbleIntent, icon)
+ .build()
+
val notificationBuilder = NotificationCompat.Builder(context, context.getString(R.string.notification_channel_chat_id))
.setSmallIcon(R.drawable.topbar_chat_notification)
.setAutoCancel(true)
@@ -680,11 +711,16 @@ class NotificationsManager(private val context: Context) {
.addAction(getMarkMessageAsReadAction(notifiable))
.setShortcutId(id)
.setLocusId(LocusIdCompat(id))
+
if (corePreferences.markAsReadUponChatMessageNotificationDismissal) {
Log.i("[Notifications Manager] Chat room will be marked as read when notification will be dismissed")
notificationBuilder
.setDeleteIntent(getMarkMessageAsReadPendingIntent(notifiable))
}
+
+ if (Compatibility.canChatMessageChannelBubble(context)) {
+ notificationBuilder.bubbleMetadata = bubble
+ }
return notificationBuilder.build()
}
diff --git a/app/src/main/res/layout/chat_bubble_activity.xml b/app/src/main/res/layout/chat_bubble_activity.xml
new file mode 100644
index 000000000..cb072b4e2
--- /dev/null
+++ b/app/src/main/res/layout/chat_bubble_activity.xml
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/chat_room_detail_fragment.xml b/app/src/main/res/layout/chat_room_detail_fragment.xml
index 52431704d..d0350a52c 100644
--- a/app/src/main/res/layout/chat_room_detail_fragment.xml
+++ b/app/src/main/res/layout/chat_room_detail_fragment.xml
@@ -185,7 +185,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
- android:layout_margin="5dp"
+ android:layout_marginTop="5dp"
+ android:layout_marginBottom="5dp"
android:layout_weight="1"
android:background="@drawable/resizable_text_field"
android:imeOptions="flagNoExtractUi"
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 04df90f9a..e136ce4f8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -707,4 +707,6 @@
Show or hide call statistics
Video attachment
Take a screenshot of received video
+ Close notification bubble
+ Open conversation in app instead of bubble