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