From 4cca59a39fdd0daf1352e5f89b8eb86602e4bf6b Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 4 Mar 2025 12:27:48 +0100 Subject: [PATCH] Use newly added APIs to get real information about a content being related to an ephemeral chat message or not, and use that information to hide save/export buttons & prevent screenshots --- CHANGELOG.md | 4 +++- README.md | 2 ++ .../java/org/linphone/ui/GenericActivity.kt | 2 +- .../ui/call/fragment/TransferCallFragment.kt | 18 --------------- .../ui/fileviewer/FileViewerActivity.kt | 8 +++++++ .../ui/fileviewer/MediaViewerActivity.kt | 23 +++++++++++++------ .../ui/fileviewer/viewmodel/FileViewModel.kt | 2 ++ .../viewmodel/MediaListViewModel.kt | 15 ++++++++++-- .../ConversationDocumentsListViewModel.kt | 13 +++++++++-- .../res/layout/file_media_viewer_activity.xml | 4 +++- .../main/res/layout/file_viewer_activity.xml | 4 +++- 11 files changed, 62 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 286c7d889..7b51c146a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ Group changes to describe their impact on the project, as follows: Security to invite users to upgrade in case of vulnerabilities. -## [6.0.0] - 2025-02-?? +## [6.0.0] - 2025-03-?? 6.0.0 release is a complete rework of Linphone Android, with a fully redesigned UI, so it is impossible to list everything here. @@ -21,6 +21,7 @@ Group changes to describe their impact on the project, as follows: - Improved multi account: you'll only see history, conversations, meetings etc... related to currently selected account, and you can switch the default account in two clicks. - Call transfer: Blind & Attended call transfer have been merged into one: during a call, if you initiate a transfer action, either pick another call to do the attended transfer or select a contact from the list (you can input a SIP URI not already in the suggestions list) to start a blind transfer. - User can only send up to 12 files in a single chat message. +- IMDNs are now only sent to the message sender, preventing huge traffic in large groups, and thus the delivery status icon for received messages is now hidden in groups (as it was in 1-1 conversations). - Settings: a lot of them are gone, the one that are still there have been reworked to increase user friendliness. - Default screen (between contacts, call history, conversations & meetings list) will change depending on where you were when the app was paused or killed, and you will return to that last visited screen on the next startup. - Gradle files have been migrated from Groovy to Kotlin DSL, and dependencies are now in a separated file (libs.versions.toml). @@ -38,6 +39,7 @@ Group changes to describe their impact on the project, as follows: - Chat while in call: a shortcut to a conversation screen with the remote. - Chat while in a conference: if the conference has a text stream enabled, you can chat with the other participants of the conference while it lasts. At the end, you'll find the messages history in the call history (and not in the list of conversations). - Auto export of media to native gallery even when auto download is enabled (but still not if VFS is enabled nor for ephemeral messages). +- Save / export document & media from ephemeral messages will be disabled, and secure policy that prevents screenshots will be enforced in file viewer even if the setting is disabled. - Notification showing upload/download of files shared through chat will let user know the progress and keep the app alive during that process. - Screen sharing in conference: only desktop app starting with 6.0 version is able to start it, but on mobiles you'll be able to see it. - You can choose whatever ringtone you'd like for incoming calls (in Android notification channel settings). diff --git a/README.md b/README.md index 99fc7f372..748fd7d8c 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,8 @@ adb logcat -d | ndk-stack -sym ./libs-debug/`adb shell getprop ro.product.cpu.ab ``` Warning: This command won't print anything until you reproduce the crash! +Starting [NDK r29](https://github.com/android/ndk/wiki/Changelog-r29) you will be able to directly use the ```libs-debug.zip``` file in ```ndk-stack -sym``` argument. + ## Create an APK with a different package name Simply edit the ```app/build.gradle.kts``` file and change the value of the ```packageName``` variable. diff --git a/app/src/main/java/org/linphone/ui/GenericActivity.kt b/app/src/main/java/org/linphone/ui/GenericActivity.kt index 6f82a5992..8d5930966 100644 --- a/app/src/main/java/org/linphone/ui/GenericActivity.kt +++ b/app/src/main/java/org/linphone/ui/GenericActivity.kt @@ -235,7 +235,7 @@ open class GenericActivity : AppCompatActivity() { startActivity(intent) } - private fun enableWindowSecureMode(enable: Boolean) { + protected fun enableWindowSecureMode(enable: Boolean) { val flags: Int = window.attributes.flags if ((enable && flags and WindowManager.LayoutParams.FLAG_SECURE != 0) || (!enable && flags and WindowManager.LayoutParams.FLAG_SECURE == 0) diff --git a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt index dc1209182..e27cb2df1 100644 --- a/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt +++ b/app/src/main/java/org/linphone/ui/call/fragment/TransferCallFragment.kt @@ -41,8 +41,6 @@ import org.linphone.ui.call.model.CallModel import org.linphone.ui.call.viewmodel.CallsViewModel import org.linphone.ui.call.viewmodel.CurrentCallViewModel import org.linphone.ui.main.adapter.ConversationsContactsAndSuggestionsListAdapter -import org.linphone.ui.main.contacts.model.ContactNumberOrAddressClickListener -import org.linphone.ui.main.contacts.model.ContactNumberOrAddressModel import org.linphone.ui.main.history.viewmodel.StartCallViewModel import org.linphone.utils.ConfirmationDialogModel import org.linphone.ui.main.model.ConversationContactOrSuggestionModel @@ -75,22 +73,6 @@ class TransferCallFragment : GenericCallFragment() { private var numberOrAddressPickerDialog: Dialog? = null - private val listener = object : ContactNumberOrAddressClickListener { - @UiThread - override fun onClicked(model: ContactNumberOrAddressModel) { - val address = model.address - if (address != null) { - coreContext.postOnCoreThread { - // TODO FIXME: transfer call (blind) - } - } - } - - @UiThread - override fun onLongPress(model: ContactNumberOrAddressModel) { - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt b/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt index 0e95f8a0b..f42a51b82 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/FileViewerActivity.kt @@ -73,11 +73,19 @@ class FileViewerActivity : GenericActivity() { return } + val isFromEphemeralMessage = args.getBoolean("isFromEphemeralMessage", false) + if (isFromEphemeralMessage) { + Log.i("$TAG Displayed content is from an ephemeral chat message, force secure mode to prevent screenshots") + // Force preventing screenshots for ephemeral messages contents + enableWindowSecureMode(true) + } + val timestamp = args.getLong("timestamp", -1) val preLoadedContent = args.getString("content") Log.i( "$TAG Path argument is [$path], pre loaded text content is ${if (preLoadedContent.isNullOrEmpty()) "not available" else "available, using it"}" ) + viewModel.isFromEphemeralMessage.value = isFromEphemeralMessage viewModel.loadFile(path, timestamp, preLoadedContent) binding.setBackClickListener { diff --git a/app/src/main/java/org/linphone/ui/fileviewer/MediaViewerActivity.kt b/app/src/main/java/org/linphone/ui/fileviewer/MediaViewerActivity.kt index e110a956e..9072c8b28 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/MediaViewerActivity.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/MediaViewerActivity.kt @@ -17,6 +17,7 @@ import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R import org.linphone.core.tools.Log import org.linphone.databinding.FileMediaViewerActivityBinding @@ -51,6 +52,13 @@ class MediaViewerActivity : GenericActivity() { val model = list[position] viewModel.currentlyDisplayedFileName.value = model.fileName viewModel.currentlyDisplayedFileDateTime.value = model.dateTime + + val isFromEphemeral = model.isFromEphemeralMessage + viewModel.isCurrentlyDisplayedFileFromEphemeralMessage.value = isFromEphemeral + if (!corePreferences.enableSecureMode) { + // Force preventing screenshots for ephemeral messages contents, but allow it for others + enableWindowSecureMode(isFromEphemeral) + } } } } @@ -96,10 +104,17 @@ class MediaViewerActivity : GenericActivity() { return } + val isFromEphemeralMessage = args.getBoolean("isFromEphemeralMessage", false) + if (isFromEphemeralMessage) { + Log.i("$TAG Displayed content is from an ephemeral chat message, force secure mode to prevent screenshots") + // Force preventing screenshots for ephemeral messages contents + enableWindowSecureMode(true) + } + val timestamp = args.getLong("timestamp", -1) val isEncrypted = args.getBoolean("isEncrypted", false) val originalPath = args.getString("originalPath", "") - val isFromEphemeralMessage = args.getBoolean("isFromEphemeralMessage", false) + Log.i("$TAG Path argument is [$path], timestamp [$timestamp], encrypted [$isEncrypted] and original path [$originalPath]") viewModel.initTempModel(path, timestamp, isEncrypted, originalPath, isFromEphemeralMessage) val conversationId = args.getString("conversationId").orEmpty() @@ -184,12 +199,6 @@ class MediaViewerActivity : GenericActivity() { val currentItem = binding.mediaViewPager.currentItem val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null if (model != null) { - // Never do auto media export for ephemeral messages! - if (model.isFromEphemeralMessage) { - Log.e("$TAG Do not export media from ephemeral message!") - return - } - val filePath = model.path lifecycleScope.launch { withContext(Dispatchers.IO) { diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt index a2fb1bb25..22b599dab 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/FileViewModel.kt @@ -69,6 +69,8 @@ class FileViewModel val dateTime = MutableLiveData() + val isFromEphemeralMessage = MutableLiveData() + val exportPlainTextFileEvent: MutableLiveData> by lazy { MutableLiveData>() } diff --git a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaListViewModel.kt b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaListViewModel.kt index 49a56714e..505e396da 100644 --- a/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/fileviewer/viewmodel/MediaListViewModel.kt @@ -41,6 +41,8 @@ class MediaListViewModel val currentlyDisplayedFileDateTime = MutableLiveData() + val isCurrentlyDisplayedFileFromEphemeralMessage = MutableLiveData() + private lateinit var temporaryModel: FileModel override fun beforeNotifyingChatRoomFound(sameOne: Boolean) { @@ -101,8 +103,17 @@ class MediaListViewModel val size = mediaContent.size.toLong() val timestamp = mediaContent.creationTimestamp if (path.isNotEmpty() && name.isNotEmpty()) { - // TODO FIXME: we don't have the ephemeral info at Content level, using the chatRoom info even if content ephemeral status may or may not be different... - val ephemeral = chatRoom.isEphemeralEnabled + val messageId = mediaContent.relatedChatMessageId + val ephemeral = if (messageId != null) { + val chatMessage = chatRoom.findMessage(messageId) + if (chatMessage == null) { + Log.w("$TAG Failed to find message using ID [$messageId] related to this content, can't get real info about being related to ephemeral message") + } + chatMessage?.isEphemeral ?: chatRoom.isEphemeralEnabled + } else { + Log.e("$TAG No chat message ID related to this content, can't get real info about being related to ephemeral message") + chatRoom.isEphemeralEnabled + } val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath, ephemeral) list.add(model) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt index 502f145f0..e703677b6 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationDocumentsListViewModel.kt @@ -79,8 +79,17 @@ class ConversationDocumentsListViewModel val size = documentContent.size.toLong() val timestamp = documentContent.creationTimestamp if (path.isNotEmpty() && name.isNotEmpty()) { - // TODO FIXME: we don't have the ephemeral info at Content level, using the chatRoom info even if content ephemeral status may or may not be different... - val ephemeral = chatRoom.isEphemeralEnabled + val messageId = documentContent.relatedChatMessageId + val ephemeral = if (messageId != null) { + val chatMessage = chatRoom.findMessage(messageId) + if (chatMessage == null) { + Log.w("$TAG Failed to find message using ID [$messageId] related to this content, can't get real info about being related to ephemeral message") + } + chatMessage?.isEphemeral ?: chatRoom.isEphemeralEnabled + } else { + Log.e("$TAG No chat message ID related to this content, can't get real info about being related to ephemeral message") + chatRoom.isEphemeralEnabled + } val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath, ephemeral) { diff --git a/app/src/main/res/layout/file_media_viewer_activity.xml b/app/src/main/res/layout/file_media_viewer_activity.xml index e02561b40..c0fde0e67 100644 --- a/app/src/main/res/layout/file_media_viewer_activity.xml +++ b/app/src/main/res/layout/file_media_viewer_activity.xml @@ -30,7 +30,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{sharedViewModel.mediaViewerFullScreenMode ? View.GONE : View.VISIBLE}" - app:constraint_referenced_ids="top_bar_background, back, file_name, share, save, date_time"/> + app:constraint_referenced_ids="top_bar_background, back, file_name, date_time"/> @@ -121,6 +122,7 @@ android:padding="15dp" android:src="@drawable/download_simple" android:contentDescription="@string/content_description_save_file" + android:visibility="@{sharedViewModel.mediaViewerFullScreenMode || viewModel.isCurrentlyDisplayedFileFromEphemeralMessage ? View.GONE : View.VISIBLE}" app:tint="@color/gray_main2_500" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/layout/file_viewer_activity.xml b/app/src/main/res/layout/file_viewer_activity.xml index f06de0fe2..cc24247b4 100644 --- a/app/src/main/res/layout/file_viewer_activity.xml +++ b/app/src/main/res/layout/file_viewer_activity.xml @@ -25,7 +25,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{viewModel.fullScreenMode ? View.GONE : View.VISIBLE}" - app:constraint_referenced_ids="top_bar_background, back, file_name, share, save, date_time"/> + app:constraint_referenced_ids="top_bar_background, back, file_name, date_time"/> @@ -159,6 +160,7 @@ android:padding="15dp" android:src="@drawable/download_simple" android:contentDescription="@string/content_description_save_file" + android:visibility="@{viewModel.fullScreenMode || viewModel.isFromEphemeralMessage ? View.GONE : View.VISIBLE}" app:tint="@color/gray_main2_500" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" />