From ab72e5eb6278750032ba74d8d69e0989561cc299 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 13 Feb 2024 11:37:17 +0100 Subject: [PATCH] Added conversation messages decorator in secured chat room to explain and show info on click --- .../chat/adapter/ConversationEventAdapter.kt | 24 +++++- .../chat/fragment/ConversationFragment.kt | 54 +++++++++++++ ...EndToEndEncryptionDetailsDialogFragment.kt | 72 +++++++++++++++++ .../utils/RecyclerViewHeaderDecoration.kt | 8 +- .../main/res/drawable/lock_simple_bold.xml | 9 +++ .../call_active_conference_fragment.xml | 1 - .../main/res/layout/call_active_fragment.xml | 1 - .../main/res/layout/call_ended_fragment.xml | 1 - ..._conversation_e2e_details_bottom_sheet.xml | 79 +++++++++++++++++++ .../res/layout/chat_conversation_event.xml | 36 ++++----- .../chat_conversation_secured_first_event.xml | 76 ++++++++++++++++++ app/src/main/res/values/strings.xml | 5 ++ 12 files changed, 341 insertions(+), 25 deletions(-) create mode 100644 app/src/main/java/org/linphone/ui/main/chat/fragment/EndToEndEncryptionDetailsDialogFragment.kt create mode 100644 app/src/main/res/drawable/lock_simple_bold.xml create mode 100644 app/src/main/res/layout/chat_conversation_e2e_details_bottom_sheet.xml create mode 100644 app/src/main/res/layout/chat_conversation_secured_first_event.xml diff --git a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt index 5f8e13472..54056b490 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt @@ -19,7 +19,9 @@ */ package org.linphone.ui.main.chat.adapter +import android.content.Context import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.core.view.doOnPreDraw import androidx.databinding.DataBindingUtil @@ -33,15 +35,19 @@ import org.linphone.core.tools.Log import org.linphone.databinding.ChatBubbleIncomingBinding import org.linphone.databinding.ChatBubbleOutgoingBinding import org.linphone.databinding.ChatConversationEventBinding +import org.linphone.databinding.ChatConversationSecuredFirstEventBinding import org.linphone.ui.main.chat.model.EventLogModel import org.linphone.ui.main.chat.model.EventModel import org.linphone.ui.main.chat.model.MessageModel import org.linphone.utils.Event +import org.linphone.utils.HeaderAdapter import org.linphone.utils.startAnimatedDrawable -class ConversationEventAdapter : ListAdapter( - EventLogDiffCallback() -) { +class ConversationEventAdapter : + ListAdapter( + EventLogDiffCallback() + ), + HeaderAdapter { companion object { private const val TAG = "[Conversation Event Adapter]" @@ -55,13 +61,25 @@ class ConversationEventAdapter : ListAdapter> by lazy { MutableLiveData>() } + val showReactionForChatMessageModelEvent: MutableLiveData> by lazy { MutableLiveData>() } + val scrollToRepliedMessageEvent: MutableLiveData> by lazy { MutableLiveData>() } + override fun displayHeaderForPosition(position: Int): Boolean { + // We only want to display it at top + return position == 0 + } + + override fun getHeaderViewForPosition(context: Context, position: Int): View { + val binding = ChatConversationSecuredFirstEventBinding.inflate(LayoutInflater.from(context)) + return binding.root + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { INCOMING_CHAT_MESSAGE -> createIncomingChatBubble(parent) diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index 778b7b26f..28124f253 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -35,6 +35,7 @@ import android.text.Editable import android.text.TextWatcher import android.view.Gravity import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.view.Window @@ -52,9 +53,11 @@ import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback +import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout.OnTabSelectedListener import java.io.File @@ -84,6 +87,7 @@ import org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel import org.linphone.ui.main.fragment.SlidingPaneChildFragment import org.linphone.utils.Event import org.linphone.utils.FileUtils +import org.linphone.utils.RecyclerViewHeaderDecoration import org.linphone.utils.RecyclerViewSwipeUtils import org.linphone.utils.RecyclerViewSwipeUtilsCallback import org.linphone.utils.TimestampUtils @@ -112,6 +116,8 @@ class ConversationFragment : SlidingPaneChildFragment() { private val args: ConversationFragmentArgs by navArgs() + private var bottomSheetDialog: BottomSheetDialogFragment? = null + private val pickMedia = registerForActivityResult( ActivityResultContracts.PickMultipleVisualMedia() ) { list -> @@ -226,6 +232,32 @@ class ConversationFragment : SlidingPaneChildFragment() { private lateinit var scrollListener: ConversationScrollListener + private lateinit var headerItemDecoration: RecyclerViewHeaderDecoration + + private val listItemTouchListener = object : RecyclerView.OnItemTouchListener { + override fun onInterceptTouchEvent( + rv: RecyclerView, + e: MotionEvent + ): Boolean { + // Following code is only to detect click on header at position 0 + if (::headerItemDecoration.isInitialized) { + if ((rv.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() == 0) { + if (e.y >= 0 && e.y <= headerItemDecoration.getDecorationHeight(0)) { + showEndToEndEncryptionDetailsBottomSheet() + return true + } + } + } + return false + } + + override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { } + + override fun onRequestDisallowInterceptTouchEvent( + disallowIntercept: Boolean + ) { } + } + private var currentChatMessageModelForBottomSheet: MessageModel? = null private val bottomSheetCallback = object : BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { @@ -344,6 +376,16 @@ class ConversationFragment : SlidingPaneChildFragment() { } else { sendMessageViewModel.configureChatRoom(viewModel.chatRoom) + if (viewModel.isEndToEndEncrypted.value == true) { + headerItemDecoration = RecyclerViewHeaderDecoration( + requireContext(), + adapter, + false + ) + binding.eventsList.addItemDecoration(headerItemDecoration) + binding.eventsList.addOnItemTouchListener(listItemTouchListener) + } + // Wait for chat room to be ready before trying to forward a message in it sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner) { event -> event.consume { toForward -> @@ -682,6 +724,9 @@ class ConversationFragment : SlidingPaneChildFragment() { override fun onPause() { super.onPause() + bottomSheetDialog?.dismiss() + bottomSheetDialog = null + if (::scrollListener.isInitialized) { binding.eventsList.removeOnScrollListener(scrollListener) } @@ -1113,4 +1158,13 @@ class ConversationFragment : SlidingPaneChildFragment() { bottomSheetAdapter.submitList(initialList) Log.i("$TAG Submitted [${initialList.size}] items for default reactions list") } + + private fun showEndToEndEncryptionDetailsBottomSheet() { + val e2eEncryptionDetailsBottomSheet = EndToEndEncryptionDetailsDialogFragment() + e2eEncryptionDetailsBottomSheet.show( + requireActivity().supportFragmentManager, + EndToEndEncryptionDetailsDialogFragment.TAG + ) + bottomSheetDialog = e2eEncryptionDetailsBottomSheet + } } diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/EndToEndEncryptionDetailsDialogFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/EndToEndEncryptionDetailsDialogFragment.kt new file mode 100644 index 000000000..55f5f7441 --- /dev/null +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/EndToEndEncryptionDetailsDialogFragment.kt @@ -0,0 +1,72 @@ +/* + * 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 . + */ +package org.linphone.ui.main.chat.fragment + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.UiThread +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.linphone.R +import org.linphone.databinding.ChatConversationE2eDetailsBottomSheetBinding + +@UiThread +class EndToEndEncryptionDetailsDialogFragment( + private val onDismiss: (() -> Unit)? = null +) : BottomSheetDialogFragment() { + companion object { + const val TAG = "EndToEndEncryptionDetailsDialogFragment" + } + + override fun onCancel(dialog: DialogInterface) { + onDismiss?.invoke() + super.onCancel(dialog) + } + + override fun onDismiss(dialog: DialogInterface) { + onDismiss?.invoke() + super.onDismiss(dialog) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog + // Makes sure all menu entries are visible, + // required for landscape mode (otherwise only first item is visible) + dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED + + // Force this navigation bar color + dialog.window?.navigationBarColor = requireContext().getColor(R.color.gray_600) + return dialog + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = ChatConversationE2eDetailsBottomSheetBinding.inflate(layoutInflater) + return view.root + } +} diff --git a/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt b/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt index 497eb964a..429fa1416 100644 --- a/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt +++ b/app/src/main/java/org/linphone/utils/RecyclerViewHeaderDecoration.kt @@ -34,6 +34,10 @@ class RecyclerViewHeaderDecoration( ) : RecyclerView.ItemDecoration() { private val headers: SparseArray = SparseArray() + fun getDecorationHeight(position: Int): Int { + return headers.get(position, null)?.height ?: 0 + } + override fun getItemOffsets( outRect: Rect, view: View, @@ -91,7 +95,9 @@ class RecyclerViewHeaderDecoration( context, position ) - canvas.translate(0f, child.y - headerView.height) + if (position != 0 || child.y < headerView.height) { + canvas.translate(0f, child.y - headerView.height) + } headerView.draw(canvas) canvas.restore() } diff --git a/app/src/main/res/drawable/lock_simple_bold.xml b/app/src/main/res/drawable/lock_simple_bold.xml new file mode 100644 index 000000000..d8cbf8026 --- /dev/null +++ b/app/src/main/res/drawable/lock_simple_bold.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/call_active_conference_fragment.xml b/app/src/main/res/layout/call_active_conference_fragment.xml index adebd1870..88d882fdb 100644 --- a/app/src/main/res/layout/call_active_conference_fragment.xml +++ b/app/src/main/res/layout/call_active_conference_fragment.xml @@ -148,7 +148,6 @@ bind:ignore="UseAppTint" /> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_conversation_event.xml b/app/src/main/res/layout/chat_conversation_event.xml index 2f4c90f32..843b3106f 100644 --- a/app/src/main/res/layout/chat_conversation_event.xml +++ b/app/src/main/res/layout/chat_conversation_event.xml @@ -13,24 +13,24 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + diff --git a/app/src/main/res/layout/chat_conversation_secured_first_event.xml b/app/src/main/res/layout/chat_conversation_secured_first_event.xml new file mode 100644 index 000000000..f98de8688 --- /dev/null +++ b/app/src/main/res/layout/chat_conversation_secured_first_event.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 104d30ed2..c2c53e79f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -431,6 +431,11 @@ Medias No media found No matching result + End-to-end encrypted conversation + Messages in this conversation are e2e encrypted. Only your correspondent can decrypt them. + Guaranteed confidentiality + Thanks to end-to-end encryption technology in &appName;, messages, calls and meetings confidentiality are guaranteed. No-one can decrypt exchanged data, not even ourselves. + https://linphone.org/security Group members Add participants