From 46ef080d62a6489ef11fca1d6b8e03376209bbb8 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 2 Feb 2022 17:19:35 +0100 Subject: [PATCH] Make SIP URI in chat messages clickable as well as http links --- CHANGELOG.md | 1 + .../org/linphone/activities/Navigation.kt | 8 ++ .../chat/adapters/ChatMessagesListAdapter.kt | 8 ++ .../main/chat/data/ChatMessageContentData.kt | 2 + .../main/chat/data/ChatMessageData.kt | 13 +++- .../chat/fragments/DetailChatRoomFragment.kt | 13 ++++ .../MultiLineWrapContentWidthTextView.kt | 7 ++ .../fragments/MasterCallLogsFragment.kt | 6 +- .../linphone/utils/PatternClickableSpan.kt | 73 +++++++++++++++++++ .../res/layout/chat_message_list_cell.xml | 1 - 10 files changed, 126 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/org/linphone/utils/PatternClickableSpan.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index fa361f48d..51272e01b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Group changes to describe their impact on the project, as follows: - Unread messages indicator in chat conversation that separates read & unread messages - Notify incoming/outgoing calls on bluetooth devices using self-managed connections from telecom manager API (disables SDK audio focus) - Ask Android to not process what user types in an encrypted chat room to improve privacy, see [IME_FLAG_NO_PERSONALIZED_LEARNING](https://developer.android.com/reference/android/view/inputmethod/EditorInfo#IME_FLAG_NO_PERSONALIZED_LEARNING) +- SIP URIs in chat messages are clickable to easily initiate a call - New video call UI on foldable device like Galaxy Z Fold - Setting to automatically record all calls - When using a physical keyboard, use left control + enter keys to send message diff --git a/app/src/main/java/org/linphone/activities/Navigation.kt b/app/src/main/java/org/linphone/activities/Navigation.kt index 1431e5a6a..64b7e3cbd 100644 --- a/app/src/main/java/org/linphone/activities/Navigation.kt +++ b/app/src/main/java/org/linphone/activities/Navigation.kt @@ -322,6 +322,14 @@ internal fun DetailChatRoomFragment.navigateToEmptyChatRoom() { ) } +internal fun DetailChatRoomFragment.navigateToDialer(args: Bundle?) { + findMasterNavController().navigate( + R.id.action_global_dialerFragment, + args, + popupTo(R.id.dialerFragment, true) + ) +} + internal fun ChatRoomCreationFragment.navigateToGroupInfo() { if (findNavController().currentDestination?.id == R.id.chatRoomCreationFragment) { findNavController().navigate( 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 dffd8aeef..12a69892f 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 @@ -86,6 +86,10 @@ class ChatMessagesListAdapter( MutableLiveData>() } + val sipUriClickedEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val scrollToChatMessageEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -94,6 +98,10 @@ class ChatMessagesListAdapter( override fun onContentClicked(content: Content) { openContentEvent.value = Event(content) } + + override fun onSipAddressClicked(sipUri: String) { + sipUriClickedEvent.value = Event(sipUri) + } } private var contextMenuDisabled: Boolean = false diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt index 7e5f9767e..fdf77d03f 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt @@ -354,4 +354,6 @@ class ChatMessageContentData( interface OnContentClickedListener { fun onContentClicked(content: Content) + + fun onSipAddressClicked(sipUri: String) } diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt index 001cce2c0..3f588d008 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt @@ -24,12 +24,14 @@ import android.text.Spannable import android.text.util.Linkify import androidx.core.text.util.LinkifyCompat import androidx.lifecycle.MutableLiveData +import java.util.regex.Pattern import org.linphone.R import org.linphone.contact.GenericContactData import org.linphone.core.ChatMessage import org.linphone.core.ChatMessageListenerStub import org.linphone.core.tools.Log import org.linphone.utils.AppUtils +import org.linphone.utils.PatternClickableSpan import org.linphone.utils.TimestampUtils class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMessage.fromAddress) { @@ -182,7 +184,16 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes } else if (content.isText) { val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim()) LinkifyCompat.addLinks(spannable, Linkify.WEB_URLS) - text.value = spannable + text.value = PatternClickableSpan() + .add( + Pattern.compile("(sips?):([^@]+)(?:@([^ ]+))?"), + object : PatternClickableSpan.SpannableClickedListener { + override fun onSpanClicked(text: String) { + Log.i("[Chat Message Data] Clicked on SIP URI: $text") + contentListener?.onSipAddressClicked(text) + } + } + ).build(spannable) } } 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 77dfadf5c..f8c2e012e 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 @@ -457,6 +457,19 @@ class DetailChatRoomFragment : MasterFragment + val args = Bundle() + args.putString("URI", sipUri) + args.putBoolean("Transfer", false) + // If auto start call setting is enabled, ignore it + args.putBoolean("SkipAutoCallStart", true) + navigateToDialer(args) + } + } + adapter.scrollToChatMessageEvent.observe( viewLifecycleOwner ) { diff --git a/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt b/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt index 059f24c9c..f06828e54 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/views/MultiLineWrapContentWidthTextView.kt @@ -21,6 +21,7 @@ package org.linphone.activities.main.chat.views import android.content.Context import android.text.Layout +import android.text.method.LinkMovementMethod import android.util.AttributeSet import androidx.appcompat.widget.AppCompatTextView import kotlin.math.ceil @@ -40,6 +41,12 @@ class MultiLineWrapContentWidthTextView : AppCompatTextView { defStyleAttr: Int ) : super(context, attrs, defStyleAttr) + override fun setText(text: CharSequence?, type: BufferType?) { + super.setText(text, type) + // Required for PatternClickableSpan + movementMethod = LinkMovementMethod.getInstance() + } + override fun onMeasure(widthSpec: Int, heightSpec: Int) { var wSpec = widthSpec val widthMode = MeasureSpec.getMode(wSpec) diff --git a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt index cd0c0d757..bbd2e0cfa 100644 --- a/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/history/fragments/MasterCallLogsFragment.kt @@ -246,10 +246,8 @@ class MasterCallLogsFragment : MasterFragment. + */ +package org.linphone.utils + +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.ClickableSpan +import android.view.View +import android.widget.TextView +import java.util.regex.Pattern + +class PatternClickableSpan { + var patterns: ArrayList = ArrayList() + + inner class SpannablePatternItem( + var pattern: Pattern, + var listener: SpannableClickedListener + ) + + interface SpannableClickedListener { + fun onSpanClicked(text: String) + } + + inner class StyledClickableSpan(var item: SpannablePatternItem) : ClickableSpan() { + override fun onClick(widget: View) { + val tv = widget as TextView + val span = tv.text as Spanned + val start = span.getSpanStart(this) + val end = span.getSpanEnd(this) + val text = span.subSequence(start, end) + item.listener.onSpanClicked(text.toString()) + } + } + + fun add( + pattern: Pattern, + listener: SpannableClickedListener + ): PatternClickableSpan { + patterns.add(SpannablePatternItem(pattern, listener)) + return this + } + + fun build(editable: CharSequence?): SpannableStringBuilder { + val ssb = SpannableStringBuilder(editable) + for (item in patterns) { + val matcher = item.pattern.matcher(ssb) + while (matcher.find()) { + val start = matcher.start() + val end = matcher.end() + val url = StyledClickableSpan(item) + ssb.setSpan(url, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } + return ssb + } +} diff --git a/app/src/main/res/layout/chat_message_list_cell.xml b/app/src/main/res/layout/chat_message_list_cell.xml index f25d32023..e9736d087 100644 --- a/app/src/main/res/layout/chat_message_list_cell.xml +++ b/app/src/main/res/layout/chat_message_list_cell.xml @@ -187,7 +187,6 @@ android:onLongClick="@{contextMenuClickListener}" android:text="@{data.text}" android:visibility="@{data.text.length > 0 ? View.VISIBLE : View.GONE}" - android:autoLink="web" android:layout_gravity="@{data.chatMessage.outgoing ? Gravity.RIGHT : Gravity.LEFT}" android:layout_width="wrap_content" android:layout_height="wrap_content"