Added back auto export media to native gallery feature

This commit is contained in:
Sylvain Berfini 2025-02-18 11:31:03 +01:00
parent 17511a4c26
commit 875198164d
21 changed files with 208 additions and 13 deletions

View file

@ -37,6 +37,7 @@ Group changes to describe their impact on the project, as follows:
- If next message is also a voice recording, playback will automatically start after the currently playing one ends. - If next message is also a voice recording, playback will automatically start after the currently playing one ends.
- Chat while in call: a shortcut to a conversation screen with the remote. - 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). - 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).
- Notification showing upload/download of files shared through chat will let user know the progress and keep the app alive during that process. - 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. - 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). - You can choose whatever ringtone you'd like for incoming calls (in Android notification channel settings).

View file

@ -46,6 +46,7 @@ import org.linphone.ui.call.CallActivity
import org.linphone.utils.ActivityMonitor import org.linphone.utils.ActivityMonitor
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.FileUtils
import org.linphone.utils.LinphoneUtils import org.linphone.utils.LinphoneUtils
class CoreContext class CoreContext
@ -112,6 +113,11 @@ class CoreContext
MutableLiveData<Event<Boolean>>() MutableLiveData<Event<Boolean>>()
} }
private var filesToExportToNativeMediaGallery = arrayListOf<String>()
val filesToExportToNativeMediaGalleryEvent: MutableLiveData<Event<List<String>>> by lazy {
MutableLiveData<Event<List<String>>>()
}
@SuppressLint("HandlerLeak") @SuppressLint("HandlerLeak")
private lateinit var coreThread: Handler private lateinit var coreThread: Handler
@ -155,6 +161,42 @@ class CoreContext
private var previousCallState = Call.State.Idle private var previousCallState = Call.State.Idle
private val coreListener = object : CoreListenerStub() { private val coreListener = object : CoreListenerStub() {
@WorkerThread
override fun onMessagesReceived(
core: Core,
chatRoom: ChatRoom,
messages: Array<out ChatMessage?>
) {
if (corePreferences.makePublicMediaFilesDownloaded && core.maxSizeForAutoDownloadIncomingFiles >= 0) {
for (message in messages) {
// Never do auto media export for ephemeral messages!
if (message?.isEphemeral == true) continue
for (content in message?.contents.orEmpty()) {
if (content.isFile) {
val path = content.filePath
if (path.isNullOrEmpty()) continue
val mime = "${content.type}/${content.subtype}"
val mimeType = FileUtils.getMimeType(mime)
when (mimeType) {
FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> {
Log.i("$TAG Added file path [$path] to the list of media to export to native media gallery")
filesToExportToNativeMediaGallery.add(path)
}
else -> {}
}
}
}
}
}
if (filesToExportToNativeMediaGallery.isNotEmpty()) {
Log.i("$TAG Creating event with [${filesToExportToNativeMediaGallery.size}] files to export to native media gallery")
filesToExportToNativeMediaGalleryEvent.postValue(Event(filesToExportToNativeMediaGallery))
}
}
@WorkerThread @WorkerThread
override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String) { override fun onGlobalStateChanged(core: Core, state: GlobalState, message: String) {
Log.i("$TAG Global state changed [$state]") Log.i("$TAG Global state changed [$state]")
@ -628,6 +670,11 @@ class CoreContext
return found != null return found != null
} }
@WorkerThread
fun clearFilesToExportToNativeGallery() {
filesToExportToNativeMediaGallery.clear()
}
@WorkerThread @WorkerThread
fun startAudioCall( fun startAudioCall(
address: Address, address: Address,

View file

@ -149,6 +149,13 @@ class CorePreferences
config.setBool("app", "mark_as_read_notif_dismissal", value) config.setBool("app", "mark_as_read_notif_dismissal", value)
} }
var makePublicMediaFilesDownloaded: Boolean
// Keep old name for backward compatibility
get() = config.getBool("app", "make_downloaded_images_public_in_gallery", false)
set(value) {
config.setBool("app", "make_downloaded_images_public_in_gallery", value)
}
// Conference related // Conference related
@get:WorkerThread @set:WorkerThread @get:WorkerThread @set:WorkerThread

View file

@ -27,6 +27,7 @@ import android.util.Base64
import android.util.Pair import android.util.Pair
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey import androidx.security.crypto.MasterKey
import org.linphone.LinphoneApplication.Companion.corePreferences
import java.math.BigInteger import java.math.BigInteger
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import java.security.KeyStore import java.security.KeyStore
@ -73,6 +74,12 @@ class VFS {
} }
preferences.edit().putBoolean("vfs_enabled", true).apply() preferences.edit().putBoolean("vfs_enabled", true).apply()
if (corePreferences.makePublicMediaFilesDownloaded) {
Log.w("$TAG VFS is now enabled, disabling auto export of media files to native gallery")
corePreferences.makePublicMediaFilesDownloaded = false
}
return true return true
} }

View file

@ -99,7 +99,8 @@ class MediaViewerActivity : GenericActivity() {
val timestamp = args.getLong("timestamp", -1) val timestamp = args.getLong("timestamp", -1)
val isEncrypted = args.getBoolean("isEncrypted", false) val isEncrypted = args.getBoolean("isEncrypted", false)
val originalPath = args.getString("originalPath", "") val originalPath = args.getString("originalPath", "")
viewModel.initTempModel(path, timestamp, isEncrypted, originalPath) val isFromEphemeralMessage = args.getBoolean("isFromEphemeralMessage", false)
viewModel.initTempModel(path, timestamp, isEncrypted, originalPath, isFromEphemeralMessage)
val conversationId = args.getString("conversationId").orEmpty() val conversationId = args.getString("conversationId").orEmpty()
Log.i( Log.i(
@ -183,6 +184,12 @@ class MediaViewerActivity : GenericActivity() {
val currentItem = binding.mediaViewPager.currentItem val currentItem = binding.mediaViewPager.currentItem
val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null val model = if (currentItem >= 0 && currentItem < list.size) list[currentItem] else null
if (model != 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 val filePath = model.path
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {

View file

@ -57,9 +57,9 @@ class MediaListViewModel
} }
@UiThread @UiThread
fun initTempModel(path: String, timestamp: Long, isEncrypted: Boolean, originalPath: String) { fun initTempModel(path: String, timestamp: Long, isEncrypted: Boolean, originalPath: String, isFromEphemeralMessage: Boolean) {
val name = FileUtils.getNameFromFilePath(path) val name = FileUtils.getNameFromFilePath(path)
val model = FileModel(path, name, 0, timestamp, isEncrypted, originalPath) val model = FileModel(path, name, 0, timestamp, isEncrypted, originalPath, isFromEphemeralMessage)
temporaryModel = model temporaryModel = model
Log.i("$TAG Temporary model for file [$name] created, use it while other media for conversation are being loaded") Log.i("$TAG Temporary model for file [$name] created, use it while other media for conversation are being loaded")
mediaList.postValue(arrayListOf(model)) mediaList.postValue(arrayListOf(model))
@ -98,7 +98,10 @@ class MediaListViewModel
val size = mediaContent.size.toLong() val size = mediaContent.size.toLong()
val timestamp = mediaContent.creationTimestamp val timestamp = mediaContent.creationTimestamp
if (path.isNotEmpty() && name.isNotEmpty()) { if (path.isNotEmpty() && name.isNotEmpty()) {
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath) // 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 model = FileModel(path, name, size, timestamp, isEncrypted, originalPath, ephemeral)
list.add(model) list.add(model)
} }

View file

@ -52,9 +52,11 @@ import androidx.navigation.NavOptions
import androidx.navigation.findNavController import androidx.navigation.findNavController
import kotlin.math.max import kotlin.math.max
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
@ -313,6 +315,19 @@ class MainActivity : GenericActivity() {
} }
} }
coreContext.filesToExportToNativeMediaGalleryEvent.observe(this) {
it.consume { files ->
Log.i("$TAG Found [${files.size}] files to export to native media gallery")
for (file in files) {
exportFileToNativeMediaGallery(file)
}
coreContext.postOnCoreThread {
coreContext.clearFilesToExportToNativeGallery()
}
}
}
CarConnection(this).type.observe(this) { CarConnection(this).type.observe(this) {
val asString = when (it) { val asString = when (it) {
CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "NOT CONNECTED" CarConnection.CONNECTION_TYPE_NOT_CONNECTED -> "NOT CONNECTED"
@ -777,4 +792,18 @@ class MainActivity : GenericActivity() {
dialog.show() dialog.show()
currentlyDisplayedAuthDialog = dialog currentlyDisplayedAuthDialog = dialog
} }
private fun exportFileToNativeMediaGallery(filePath: String) {
lifecycleScope.launch {
withContext(Dispatchers.IO) {
Log.i("$TAG Export file [$filePath] to Android's MediaStore")
val mediaStorePath = FileUtils.addContentToMediaStore(filePath)
if (mediaStorePath.isNotEmpty()) {
Log.i("$TAG File [$filePath] has been successfully exported to MediaStore")
} else {
Log.e("$TAG Failed to export file [$filePath] to MediaStore!")
}
}
}
}
} }

View file

@ -145,6 +145,7 @@ class ConversationDocumentsListFragment : SlidingPaneChildFragment() {
putBoolean("isEncrypted", fileModel.isEncrypted) putBoolean("isEncrypted", fileModel.isEncrypted)
putLong("timestamp", fileModel.fileCreationTimestamp) putLong("timestamp", fileModel.fileCreationTimestamp)
putString("originalPath", fileModel.originalPath) putString("originalPath", fileModel.originalPath)
putBoolean("isFromEphemeralMessage", fileModel.isFromEphemeralMessage)
putBoolean("isMedia", false) putBoolean("isMedia", false)
} }
when (FileUtils.getMimeType(mime)) { when (FileUtils.getMimeType(mime)) {

View file

@ -1114,6 +1114,7 @@ open class ConversationFragment : SlidingPaneChildFragment() {
putBoolean("isEncrypted", fileModel.isEncrypted) putBoolean("isEncrypted", fileModel.isEncrypted)
putLong("timestamp", fileModel.fileCreationTimestamp) putLong("timestamp", fileModel.fileCreationTimestamp)
putString("originalPath", fileModel.originalPath) putString("originalPath", fileModel.originalPath)
putBoolean("isFromEphemeralMessage", fileModel.isFromEphemeralMessage)
} }
when (mimeType) { when (mimeType) {
FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> { FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> {

View file

@ -174,6 +174,7 @@ class ConversationMediaListFragment : SlidingPaneChildFragment() {
putBoolean("isEncrypted", fileModel.isEncrypted) putBoolean("isEncrypted", fileModel.isEncrypted)
putLong("timestamp", fileModel.fileCreationTimestamp) putLong("timestamp", fileModel.fileCreationTimestamp)
putString("originalPath", fileModel.originalPath) putString("originalPath", fileModel.originalPath)
putBoolean("isFromEphemeralMessage", fileModel.isFromEphemeralMessage)
putBoolean("isMedia", true) putBoolean("isMedia", true)
} }
when (FileUtils.getMimeType(mime)) { when (FileUtils.getMimeType(mime)) {

View file

@ -38,7 +38,8 @@ class EventLogModel
onWebUrlClicked: ((url: String) -> Unit)? = null, onWebUrlClicked: ((url: String) -> Unit)? = null,
onContactClicked: ((friendRefKey: String) -> Unit)? = null, onContactClicked: ((friendRefKey: String) -> Unit)? = null,
onRedToastToShow: ((pair: Pair<Int, Int>) -> Unit)? = null, onRedToastToShow: ((pair: Pair<Int, Int>) -> Unit)? = null,
onVoiceRecordingPlaybackEnded: ((id: String) -> Unit)? = null onVoiceRecordingPlaybackEnded: ((id: String) -> Unit)? = null,
onFileToExportToNativeGallery: ((path: String) -> Unit)? = null
) { ) {
companion object { companion object {
private const val TAG = "[Event Log Model]" private const val TAG = "[Event Log Model]"
@ -89,7 +90,8 @@ class EventLogModel
onWebUrlClicked, onWebUrlClicked,
onContactClicked, onContactClicked,
onRedToastToShow, onRedToastToShow,
onVoiceRecordingPlaybackEnded onVoiceRecordingPlaybackEnded,
onFileToExportToNativeGallery
) )
} }

View file

@ -45,6 +45,7 @@ class FileModel
val fileCreationTimestamp: Long, val fileCreationTimestamp: Long,
val isEncrypted: Boolean, val isEncrypted: Boolean,
val originalPath: String, val originalPath: String,
val isFromEphemeralMessage: Boolean,
val isWaitingToBeDownloaded: Boolean = false, val isWaitingToBeDownloaded: Boolean = false,
val flexboxLayoutWrapBefore: Boolean = false, val flexboxLayoutWrapBefore: Boolean = false,
private val onClicked: ((model: FileModel) -> Unit)? = null private val onClicked: ((model: FileModel) -> Unit)? = null

View file

@ -42,6 +42,7 @@ import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R import org.linphone.R
import org.linphone.core.Address import org.linphone.core.Address
import org.linphone.core.ChatMessage import org.linphone.core.ChatMessage
@ -82,7 +83,8 @@ class MessageModel
private val onWebUrlClicked: ((url: String) -> Unit)? = null, private val onWebUrlClicked: ((url: String) -> Unit)? = null,
private val onContactClicked: ((friendRefKey: String) -> Unit)? = null, private val onContactClicked: ((friendRefKey: String) -> Unit)? = null,
private val onRedToastToShow: ((pair: Pair<Int, Int>) -> Unit)? = null, private val onRedToastToShow: ((pair: Pair<Int, Int>) -> Unit)? = null,
private val onVoiceRecordingPlaybackEnded: ((id: String) -> Unit)? = null private val onVoiceRecordingPlaybackEnded: ((id: String) -> Unit)? = null,
private val onFileToExportToNativeGallery: ((path: String) -> Unit)? = null
) { ) {
companion object { companion object {
private const val TAG = "[Message Model]" private const val TAG = "[Message Model]"
@ -224,6 +226,27 @@ class MessageModel
} }
} }
@WorkerThread
override fun onFileTransferTerminated(message: ChatMessage, content: Content) {
Log.i("$TAG File [${content.name}] from message [${message.messageId}] transfer terminated")
// Never do auto media export for ephemeral messages!
if (corePreferences.makePublicMediaFilesDownloaded && !message.isEphemeral) {
val path = content.filePath
if (path.isNullOrEmpty()) return
val mime = "${content.type}/${content.subtype}"
val mimeType = FileUtils.getMimeType(mime)
when (mimeType) {
FileUtils.MimeType.Image, FileUtils.MimeType.Video, FileUtils.MimeType.Audio -> {
Log.i("$TAG Exporting file path [$path] to the native media gallery")
onFileToExportToNativeGallery?.invoke(path)
}
else -> {}
}
}
}
@WorkerThread @WorkerThread
override fun onNewMessageReaction(message: ChatMessage, reaction: ChatMessageReaction) { override fun onNewMessageReaction(message: ChatMessage, reaction: ChatMessageReaction) {
Log.i( Log.i(
@ -443,6 +466,7 @@ class MessageModel
timestamp, timestamp,
isFileEncrypted, isFileEncrypted,
originalPath, originalPath,
chatMessage.isEphemeral,
flexboxLayoutWrapBefore = wrapBefore flexboxLayoutWrapBefore = wrapBefore
) { model -> ) { model ->
onContentClicked?.invoke(model) onContentClicked?.invoke(model)
@ -470,7 +494,8 @@ class MessageModel
content.fileSize.toLong(), content.fileSize.toLong(),
timestamp, timestamp,
isFileEncrypted, isFileEncrypted,
path path,
chatMessage.isEphemeral
) { model -> ) { model ->
onContentClicked?.invoke(model) onContentClicked?.invoke(model)
} }
@ -482,6 +507,7 @@ class MessageModel
timestamp, timestamp,
isFileEncrypted, isFileEncrypted,
name, name,
chatMessage.isEphemeral,
isWaitingToBeDownloaded = true isWaitingToBeDownloaded = true
) { model -> ) { model ->
downloadContent(model, content) downloadContent(model, content)

View file

@ -79,7 +79,11 @@ class ConversationDocumentsListViewModel
val size = documentContent.size.toLong() val size = documentContent.size.toLong()
val timestamp = documentContent.creationTimestamp val timestamp = documentContent.creationTimestamp
if (path.isNotEmpty() && name.isNotEmpty()) { if (path.isNotEmpty() && name.isNotEmpty()) {
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath) { // 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 model =
FileModel(path, name, size, timestamp, isEncrypted, originalPath, ephemeral) {
openDocumentEvent.postValue(Event(it)) openDocumentEvent.postValue(Event(it))
} }
list.add(model) list.add(model)

View file

@ -78,7 +78,8 @@ class ConversationMediaListViewModel
val size = mediaContent.size.toLong() val size = mediaContent.size.toLong()
val timestamp = mediaContent.creationTimestamp val timestamp = mediaContent.creationTimestamp
if (path.isNotEmpty() && name.isNotEmpty()) { if (path.isNotEmpty() && name.isNotEmpty()) {
val model = FileModel(path, name, size, timestamp, isEncrypted, originalPath) { val model =
FileModel(path, name, size, timestamp, isEncrypted, originalPath, chatRoom.isEphemeralEnabled) {
openMediaEvent.postValue(Event(it)) openMediaEvent.postValue(Event(it))
} }
list.add(model) list.add(model)

View file

@ -761,6 +761,19 @@ class ConversationViewModel
}, },
{ id -> { id ->
voiceRecordPlaybackEndedEvent.postValue(Event(id)) voiceRecordPlaybackEndedEvent.postValue(Event(id))
},
{ filePath ->
viewModelScope.launch {
withContext(Dispatchers.IO) {
Log.i("$TAG Export file [$filePath] to Android's MediaStore")
val mediaStorePath = FileUtils.addContentToMediaStore(filePath)
if (mediaStorePath.isNotEmpty()) {
Log.i("$TAG File [$filePath] has been successfully exported to MediaStore")
} else {
Log.e("$TAG Failed to export file [$filePath] to MediaStore!")
}
}
}
} }
) )
eventsList.add(model) eventsList.add(model)

View file

@ -397,7 +397,7 @@ class SendMessageInConversationViewModel
val fileName = FileUtils.getNameFromFilePath(file) val fileName = FileUtils.getNameFromFilePath(file)
val timestamp = System.currentTimeMillis() / 1000 val timestamp = System.currentTimeMillis() / 1000
val model = FileModel(file, fileName, 0, timestamp, false, file) { model -> val model = FileModel(file, fileName, 0, timestamp, false, file, chatRoom.isEphemeralEnabled) { model ->
removeAttachment(model.path) removeAttachment(model.path)
} }

View file

@ -90,6 +90,8 @@ class SettingsViewModel
val autoDownloadEnabled = MutableLiveData<Boolean>() val autoDownloadEnabled = MutableLiveData<Boolean>()
val autoExportMediaToNativeGallery = MutableLiveData<Boolean>()
val markAsReadWhenDismissingNotification = MutableLiveData<Boolean>() val markAsReadWhenDismissingNotification = MutableLiveData<Boolean>()
// Contacts settings // Contacts settings
@ -246,7 +248,8 @@ class SettingsViewModel
expandAudioCodecs.value = false expandAudioCodecs.value = false
expandVideoCodecs.value = false expandVideoCodecs.value = false
isVfsEnabled.value = VFS.isEnabled(coreContext.context) val vfsEnabled = VFS.isEnabled(coreContext.context)
isVfsEnabled.value = vfsEnabled
val vibrator = coreContext.context.getSystemService(Vibrator::class.java) val vibrator = coreContext.context.getSystemService(Vibrator::class.java)
isVibrationAvailable.value = vibrator.hasVibrator() isVibrationAvailable.value = vibrator.hasVibrator()
@ -285,6 +288,7 @@ class SettingsViewModel
allowIpv6.postValue(core.isIpv6Enabled) allowIpv6.postValue(core.isIpv6Enabled)
autoDownloadEnabled.postValue(core.maxSizeForAutoDownloadIncomingFiles == 0) autoDownloadEnabled.postValue(core.maxSizeForAutoDownloadIncomingFiles == 0)
autoExportMediaToNativeGallery.postValue(corePreferences.makePublicMediaFilesDownloaded && !vfsEnabled)
markAsReadWhenDismissingNotification.postValue( markAsReadWhenDismissingNotification.postValue(
corePreferences.markConversationAsReadWhenDismissingMessageNotification corePreferences.markConversationAsReadWhenDismissingMessageNotification
) )
@ -439,6 +443,15 @@ class SettingsViewModel
} }
} }
@UiThread
fun toggleAutoExportMediaFilesToNativeGallery() {
val newValue = autoExportMediaToNativeGallery.value == false
coreContext.postOnCoreThread { core ->
corePreferences.makePublicMediaFilesDownloaded = newValue
autoExportMediaToNativeGallery.postValue(newValue)
}
}
@UiThread @UiThread
fun toggleMarkConversationAsReadWhenDismissingNotification() { fun toggleMarkConversationAsReadWhenDismissingNotification() {
val newValue = markAsReadWhenDismissingNotification.value == false val newValue = markAsReadWhenDismissingNotification.value == false

View file

@ -40,10 +40,39 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="20dp" android:layout_marginTop="20dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:enabled="@{!viewModel.isVfsEnabled}"
android:checked="@{viewModel.autoDownloadEnabled}" android:checked="@{viewModel.autoDownloadEnabled}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:onClick="@{() -> viewModel.toggleAutoExportMediaFilesToNativeGallery()}"
android:id="@+id/auto_export_media_to_native_gallery_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/settings_conversations_auto_export_media_to_native_gallery_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="@id/auto_export_media_to_native_gallery_switch"
app:layout_constraintBottom_toBottomOf="@id/auto_export_media_to_native_gallery_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/auto_export_media_to_native_gallery_switch"/>
<com.google.android.material.materialswitch.MaterialSwitch
style="@style/material_switch_style"
android:id="@+id/auto_export_media_to_native_gallery_switch"
android:onClick="@{() -> viewModel.toggleAutoExportMediaFilesToNativeGallery()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:checked="@{viewModel.autoExportMediaToNativeGallery}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/auto_download_switch" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style" style="@style/settings_title_style"
android:onClick="@{() -> viewModel.toggleMarkConversationAsReadWhenDismissingNotification()}" android:onClick="@{() -> viewModel.toggleMarkConversationAsReadWhenDismissingNotification()}"
@ -70,7 +99,7 @@
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:checked="@{viewModel.markAsReadWhenDismissingNotification}" android:checked="@{viewModel.markAsReadWhenDismissingNotification}"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/auto_download_switch" /> app:layout_constraintTop_toBottomOf="@id/auto_export_media_to_native_gallery_switch" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -201,6 +201,7 @@
<string name="settings_calls_change_ringtone_title">Changer de sonnerie</string> <string name="settings_calls_change_ringtone_title">Changer de sonnerie</string>
<string name="settings_conversations_title">Conversations</string> <string name="settings_conversations_title">Conversations</string>
<string name="settings_conversations_auto_download_title">Télécharger automatiquement les fichiers</string> <string name="settings_conversations_auto_download_title">Télécharger automatiquement les fichiers</string>
<string name="settings_conversations_auto_export_media_to_native_gallery_title">Rendre visible dans la galerie les médias téléchargés</string>
<string name="settings_conversations_mark_as_read_when_dismissing_notif_title">Marquer la conversation comme lue lorsqu\'une notification de message est supprimée</string> <string name="settings_conversations_mark_as_read_when_dismissing_notif_title">Marquer la conversation comme lue lorsqu\'une notification de message est supprimée</string>
<string name="settings_contacts_title">Contacts</string> <string name="settings_contacts_title">Contacts</string>
<string name="settings_contacts_add_ldap_server_title">Ajouter un serveur LDAP</string> <string name="settings_contacts_add_ldap_server_title">Ajouter un serveur LDAP</string>

View file

@ -240,6 +240,7 @@
<string name="settings_calls_change_ringtone_title">Change ringtone</string> <string name="settings_calls_change_ringtone_title">Change ringtone</string>
<string name="settings_conversations_title">Conversations</string> <string name="settings_conversations_title">Conversations</string>
<string name="settings_conversations_auto_download_title">Auto-download files</string> <string name="settings_conversations_auto_download_title">Auto-download files</string>
<string name="settings_conversations_auto_export_media_to_native_gallery_title">Make downloaded media public</string>
<string name="settings_conversations_mark_as_read_when_dismissing_notif_title">Mark conversation as read when dismissing message notification</string> <string name="settings_conversations_mark_as_read_when_dismissing_notif_title">Mark conversation as read when dismissing message notification</string>
<string name="settings_contacts_title">Contacts</string> <string name="settings_contacts_title">Contacts</string>
<string name="settings_contacts_add_ldap_server_title">Add LDAP server</string> <string name="settings_contacts_add_ldap_server_title">Add LDAP server</string>