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

This commit is contained in:
Sylvain Berfini 2025-03-04 12:27:48 +01:00
parent 4e852601fc
commit 4cca59a39f
11 changed files with 62 additions and 33 deletions

View file

@ -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).

View file

@ -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.

View file

@ -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)

View file

@ -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)

View file

@ -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 {

View file

@ -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) {

View file

@ -69,6 +69,8 @@ class FileViewModel
val dateTime = MutableLiveData<String>()
val isFromEphemeralMessage = MutableLiveData<Boolean>()
val exportPlainTextFileEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}

View file

@ -41,6 +41,8 @@ class MediaListViewModel
val currentlyDisplayedFileDateTime = MutableLiveData<String>()
val isCurrentlyDisplayedFileFromEphemeralMessage = MutableLiveData<Boolean>()
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)

View file

@ -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) {

View file

@ -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"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/top_bar_barrier"
@ -108,6 +108,7 @@
android:padding="15dp"
android:src="@drawable/share_network"
android:contentDescription="@string/content_description_share_file"
android:visibility="@{sharedViewModel.mediaViewerFullScreenMode || viewModel.isCurrentlyDisplayedFileFromEphemeralMessage ? View.GONE : View.VISIBLE}"
app:tint="@color/gray_main2_500"
app:layout_constraintEnd_toStartOf="@id/save"
app:layout_constraintTop_toTopOf="parent" />
@ -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" />

View file

@ -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"/>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/top_bar_barrier"
@ -146,6 +146,7 @@
android:padding="15dp"
android:src="@drawable/share_network"
android:contentDescription="@string/content_description_share_file"
android:visibility="@{viewModel.fullScreenMode || viewModel.isFromEphemeralMessage ? View.GONE : View.VISIBLE}"
app:tint="@color/gray_main2_500"
app:layout_constraintEnd_toStartOf="@id/save"
app:layout_constraintTop_toTopOf="parent" />
@ -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" />