From 281b44a2400060c56e9b4c2d4f210447408bc89f Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Wed, 10 Jan 2024 16:20:57 +0100 Subject: [PATCH] Added conversations & meetings settings --- .../java/org/linphone/core/CorePreferences.kt | 9 ++ .../settings/fragment/SettingsFragment.kt | 28 ++++ .../settings/viewmodel/SettingsViewModel.kt | 64 +++++++-- .../ui/main/viewer/viewmodel/FileViewModel.kt | 127 +----------------- .../main/java/org/linphone/utils/FileUtils.kt | 120 +++++++++++++++++ app/src/main/res/layout/settings_chat.xml | 70 ++++++++++ app/src/main/res/layout/settings_meetings.xml | 38 ++++++ app/src/main/res/values/strings.xml | 6 + 8 files changed, 324 insertions(+), 138 deletions(-) diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index f37cd45ca..f70557495 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -100,6 +100,15 @@ class CorePreferences @UiThread constructor(private val context: Context) { config.setBool("app", "auto_start_call_record", value) } + // Conversation settings + + var exportMediaToNativeGallery: Boolean // TODO: use it! + // Keep old name for backward compatibility + get() = config.getBool("app", "make_downloaded_images_public_in_gallery", true) + set(value) { + config.setBool("app", "make_downloaded_images_public_in_gallery", value) + } + /* Voice Recordings */ var voiceRecordingMaxDuration: Int diff --git a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt index 2ce3494f4..2292113ea 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/fragment/SettingsFragment.kt @@ -38,6 +38,18 @@ class SettingsFragment : GenericFragment() { } } + private val layoutListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + val label = viewModel.availableLayoutsNames[position] + val value = viewModel.availableLayoutsValues[position] + Log.i("$TAG Selected meeting default layout is now [$label] ($value)") + viewModel.setDefaultLayout(value) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + } + } + private val themeListener = object : AdapterView.OnItemSelectedListener { override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { val label = viewModel.availableThemesNames[position] @@ -97,6 +109,22 @@ class SettingsFragment : GenericFragment() { binding.callsSettings.deviceRingtoneSpinner.onItemSelectedListener = ringtoneListener + // Meeting default layout related + val layoutAdapter = ArrayAdapter( + requireContext(), + R.layout.drop_down_item, + viewModel.availableLayoutsNames + ) + layoutAdapter.setDropDownViewResource(R.layout.generic_dropdown_cell) + binding.meetingsSettings.layoutSpinner.adapter = layoutAdapter + + viewModel.defaultLayout.observe(viewLifecycleOwner) { layout -> + binding.meetingsSettings.layoutSpinner.setSelection( + viewModel.availableLayoutsValues.indexOf(layout) + ) + } + binding.meetingsSettings.layoutSpinner.onItemSelectedListener = layoutListener + // Light/Dark theme related val themeAdapter = ArrayAdapter( requireContext(), diff --git a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt index d062ea524..0b84174d2 100644 --- a/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/settings/viewmodel/SettingsViewModel.kt @@ -34,6 +34,7 @@ import java.util.Locale import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.LinphoneApplication.Companion.corePreferences import org.linphone.R +import org.linphone.core.Conference import org.linphone.core.Player import org.linphone.core.PlayerListener import org.linphone.core.tools.Log @@ -71,16 +72,32 @@ class SettingsViewModel @UiThread constructor() : ViewModel() { // Conversations settings val showConversationsSettings = MutableLiveData() + val autoDownloadEnabled = MutableLiveData() + val exportMediaEnabled = MutableLiveData() + // Meetings settings val showMeetingsSettings = MutableLiveData() + val defaultLayout = MutableLiveData() + val availableLayoutsNames = arrayListOf( + AppUtils.getString(R.string.settings_meetings_layout_active_speaker_label), + AppUtils.getString(R.string.settings_meetings_layout_mosaic_label) + ) + val availableLayoutsValues = arrayListOf( + Conference.Layout.ActiveSpeaker.toInt(), + Conference.Layout.Grid.toInt() + ) + // Network settings val useWifiOnly = MutableLiveData() // User Interface settings - val theme = MutableLiveData() - val availableThemesNames = arrayListOf() + val availableThemesNames = arrayListOf( + AppUtils.getString(R.string.settings_user_interface_auto_theme_label), + AppUtils.getString(R.string.settings_user_interface_light_theme_label), + AppUtils.getString(R.string.settings_user_interface_dark_theme_label) + ) val availableThemesValues = arrayListOf(-1, 0, 1) // Other @@ -114,16 +131,6 @@ class SettingsViewModel @UiThread constructor() : ViewModel() { computeAvailableRingtones() - availableThemesNames.add( - AppUtils.getString(R.string.settings_user_interface_auto_theme_label) - ) - availableThemesNames.add( - AppUtils.getString(R.string.settings_user_interface_light_theme_label) - ) - availableThemesNames.add( - AppUtils.getString(R.string.settings_user_interface_dark_theme_label) - ) - coreContext.postOnCoreThread { core -> echoCancellerEnabled.postValue(core.isEchoCancellationEnabled) routeAudioToBluetooth.postValue(corePreferences.routeAudioToBluetoothIfAvailable) @@ -137,6 +144,11 @@ class SettingsViewModel @UiThread constructor() : ViewModel() { Log.i("Currently configured ringtone in Core is [$ringtone]") selectedRingtone.postValue(ringtone) + autoDownloadEnabled.postValue(core.maxSizeForAutoDownloadIncomingFiles == 0) + exportMediaEnabled.postValue(corePreferences.exportMediaToNativeGallery) + + defaultLayout.postValue(core.defaultConferenceLayout.toInt()) + theme.postValue(corePreferences.darkMode) } } @@ -296,11 +308,39 @@ class SettingsViewModel @UiThread constructor() : ViewModel() { expandConversations.value = expandConversations.value == false } + @UiThread + fun toggleAutoDownload() { + val newValue = autoDownloadEnabled.value == false + coreContext.postOnCoreThread { core -> + core.maxSizeForAutoDownloadIncomingFiles = if (newValue) 0 else -1 + autoDownloadEnabled.postValue(newValue) + } + } + + @UiThread + fun toggleExportMedia() { + val newValue = exportMediaEnabled.value == false + coreContext.postOnCoreThread { + corePreferences.exportMediaToNativeGallery = newValue + exportMediaEnabled.postValue(newValue) + } + } + @UiThread fun toggleMeetingsExpand() { expandMeetings.value = expandMeetings.value == false } + @UiThread + fun setDefaultLayout(layoutValue: Int) { + coreContext.postOnCoreThread { core -> + val newDefaultLayout = Conference.Layout.fromInt(layoutValue) + core.defaultConferenceLayout = newDefaultLayout + Log.i("$TAG Default meeting layout [$newDefaultLayout] saved") + defaultLayout.postValue(layoutValue) + } + } + @UiThread fun toggleNetworkExpand() { expandNetwork.value = expandNetwork.value == false diff --git a/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt b/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt index 8b0bc9d44..31c92dc67 100644 --- a/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/viewer/viewmodel/FileViewModel.kt @@ -1,13 +1,9 @@ package org.linphone.ui.main.viewer.viewmodel -import android.content.ContentValues -import android.content.Context import android.graphics.Bitmap import android.graphics.pdf.PdfRenderer import android.net.Uri -import android.os.Environment import android.os.ParcelFileDescriptor -import android.provider.MediaStore import android.widget.ImageView import androidx.annotation.UiThread import androidx.lifecycle.MutableLiveData @@ -21,9 +17,7 @@ import java.lang.StringBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R -import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log import org.linphone.utils.AppUtils import org.linphone.utils.Event @@ -208,7 +202,7 @@ class FileViewModel @UiThread constructor() : ViewModel() { viewModelScope.launch { withContext(Dispatchers.IO) { Log.i("$TAG Export file [$filePath] to Android's MediaStore") - val mediaStorePath = addContentToMediaStore(filePath) + val mediaStorePath = FileUtils.addContentToMediaStore(filePath) if (mediaStorePath.isNotEmpty()) { Log.i( "$TAG File [$filePath] has been successfully exported to MediaStore" @@ -302,123 +296,4 @@ class FileViewModel @UiThread constructor() : ViewModel() { } } } - - @UiThread - private suspend fun addContentToMediaStore( - path: String - ): String { - if (path.isEmpty()) { - Log.e("$TAG No file path to export to MediaStore!") - return "" - } - - val isImage = FileUtils.isExtensionImage(path) - val isVideo = FileUtils.isExtensionVideo(path) - val isAudio = FileUtils.isExtensionAudio(path) - - val directory = when { - isImage -> Environment.DIRECTORY_PICTURES - isVideo -> Environment.DIRECTORY_MOVIES - isAudio -> Environment.DIRECTORY_MUSIC - else -> Environment.DIRECTORY_DOWNLOADS - } - - val appName = AppUtils.getString(R.string.app_name) - val relativePath = "$directory/$appName" - val fileName = FileUtils.getNameFromFilePath(path) - val extension = FileUtils.getExtensionFromFileName(fileName) - val mime = FileUtils.getMimeTypeFromExtension(extension) - - val context = coreContext.context - val mediaStoreFilePath = when { - isImage -> { - val values = ContentValues().apply { - put(MediaStore.Images.Media.DISPLAY_NAME, fileName) - put(MediaStore.Images.Media.MIME_TYPE, mime) - put(MediaStore.Images.Media.RELATIVE_PATH, relativePath) - put(MediaStore.Images.Media.IS_PENDING, 1) - } - val collection = Compatibility.getMediaCollectionUri(isImage = true) - addContentValuesToCollection( - context, - path, - collection, - values, - MediaStore.Images.Media.IS_PENDING - ) - } - isVideo -> { - val values = ContentValues().apply { - put(MediaStore.Video.Media.TITLE, fileName) - put(MediaStore.Video.Media.DISPLAY_NAME, fileName) - put(MediaStore.Video.Media.MIME_TYPE, mime) - put(MediaStore.Video.Media.RELATIVE_PATH, relativePath) - put(MediaStore.Video.Media.IS_PENDING, 1) - } - val collection = Compatibility.getMediaCollectionUri(isVideo = true) - addContentValuesToCollection( - context, - path, - collection, - values, - MediaStore.Video.Media.IS_PENDING - ) - } - isAudio -> { - val values = ContentValues().apply { - put(MediaStore.Audio.Media.TITLE, fileName) - put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) - put(MediaStore.Audio.Media.MIME_TYPE, mime) - put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath) - put(MediaStore.Audio.Media.IS_PENDING, 1) - } - val collection = Compatibility.getMediaCollectionUri(isAudio = true) - addContentValuesToCollection( - context, - path, - collection, - values, - MediaStore.Audio.Media.IS_PENDING - ) - } - else -> "" - } - - if (mediaStoreFilePath.isNotEmpty()) { - Log.i("$TAG Exported file path to MediaStore is: $mediaStoreFilePath") - return mediaStoreFilePath - } - - return "" - } - - @UiThread - private suspend fun addContentValuesToCollection( - context: Context, - filePath: String, - collection: Uri, - values: ContentValues, - pendingKey: String - ): String { - try { - val fileUri = context.contentResolver.insert(collection, values) - if (fileUri == null) { - Log.e("$TAG Failed to get a URI to where store the file, aborting") - return "" - } - - context.contentResolver.openOutputStream(fileUri).use { out -> - if (FileUtils.copyFileTo(filePath, out)) { - values.clear() - values.put(pendingKey, 0) - context.contentResolver.update(fileUri, values, null, null) - - return fileUri.toString() - } - } - } catch (e: Exception) { - Log.e("$TAG Exception: $e") - } - return "" - } } diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index 715ab2c35..6c52ef3f9 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -19,17 +19,20 @@ */ package org.linphone.utils +import android.content.ContentValues import android.content.Context import android.database.CursorIndexOutOfBoundsException import android.net.Uri import android.os.Environment import android.os.ParcelFileDescriptor import android.os.Process +import android.provider.MediaStore import android.provider.OpenableColumns import android.system.Os import android.text.format.Formatter import android.webkit.MimeTypeMap import androidx.annotation.AnyThread +import androidx.annotation.UiThread import androidx.core.content.FileProvider import java.io.File import java.io.FileInputStream @@ -42,6 +45,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R +import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log class FileUtils { @@ -478,5 +482,121 @@ class FileUtils { } return name } + + suspend fun addContentToMediaStore(path: String): String { + if (path.isEmpty()) { + Log.e("$TAG No file path to export to MediaStore!") + return "" + } + + val isImage = isExtensionImage(path) + val isVideo = isExtensionVideo(path) + val isAudio = isExtensionAudio(path) + + val directory = when { + isImage -> Environment.DIRECTORY_PICTURES + isVideo -> Environment.DIRECTORY_MOVIES + isAudio -> Environment.DIRECTORY_MUSIC + else -> Environment.DIRECTORY_DOWNLOADS + } + + val appName = AppUtils.getString(R.string.app_name) + val relativePath = "$directory/$appName" + val fileName = getNameFromFilePath(path) + val extension = getExtensionFromFileName(fileName) + val mime = getMimeTypeFromExtension(extension) + + val context = coreContext.context + val mediaStoreFilePath = when { + isImage -> { + val values = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, fileName) + put(MediaStore.Images.Media.MIME_TYPE, mime) + put(MediaStore.Images.Media.RELATIVE_PATH, relativePath) + put(MediaStore.Images.Media.IS_PENDING, 1) + } + val collection = Compatibility.getMediaCollectionUri(isImage = true) + addContentValuesToCollection( + context, + path, + collection, + values, + MediaStore.Images.Media.IS_PENDING + ) + } + isVideo -> { + val values = ContentValues().apply { + put(MediaStore.Video.Media.TITLE, fileName) + put(MediaStore.Video.Media.DISPLAY_NAME, fileName) + put(MediaStore.Video.Media.MIME_TYPE, mime) + put(MediaStore.Video.Media.RELATIVE_PATH, relativePath) + put(MediaStore.Video.Media.IS_PENDING, 1) + } + val collection = Compatibility.getMediaCollectionUri(isVideo = true) + addContentValuesToCollection( + context, + path, + collection, + values, + MediaStore.Video.Media.IS_PENDING + ) + } + isAudio -> { + val values = ContentValues().apply { + put(MediaStore.Audio.Media.TITLE, fileName) + put(MediaStore.Audio.Media.DISPLAY_NAME, fileName) + put(MediaStore.Audio.Media.MIME_TYPE, mime) + put(MediaStore.Audio.Media.RELATIVE_PATH, relativePath) + put(MediaStore.Audio.Media.IS_PENDING, 1) + } + val collection = Compatibility.getMediaCollectionUri(isAudio = true) + addContentValuesToCollection( + context, + path, + collection, + values, + MediaStore.Audio.Media.IS_PENDING + ) + } + else -> "" + } + + if (mediaStoreFilePath.isNotEmpty()) { + Log.i("$TAG Exported file path to MediaStore is: $mediaStoreFilePath") + return mediaStoreFilePath + } + + return "" + } + + @UiThread + private suspend fun addContentValuesToCollection( + context: Context, + filePath: String, + collection: Uri, + values: ContentValues, + pendingKey: String + ): String { + try { + val fileUri = context.contentResolver.insert(collection, values) + if (fileUri == null) { + Log.e("$TAG Failed to get a URI to where store the file, aborting") + return "" + } + + context.contentResolver.openOutputStream(fileUri).use { out -> + if (copyFileTo(filePath, out)) { + values.clear() + values.put(pendingKey, 0) + context.contentResolver.update(fileUri, values, null, null) + + return fileUri.toString() + } + } + } catch (e: Exception) { + Log.e("$TAG Exception: $e") + } + return "" + } } } diff --git a/app/src/main/res/layout/settings_chat.xml b/app/src/main/res/layout/settings_chat.xml index ee4918ee9..a32813ceb 100644 --- a/app/src/main/res/layout/settings_chat.xml +++ b/app/src/main/res/layout/settings_chat.xml @@ -16,6 +16,76 @@ android:paddingBottom="20dp" android:background="@drawable/shape_squircle_white_background"> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_meetings.xml b/app/src/main/res/layout/settings_meetings.xml index ee4918ee9..cdf87166a 100644 --- a/app/src/main/res/layout/settings_meetings.xml +++ b/app/src/main/res/layout/settings_meetings.xml @@ -16,6 +16,44 @@ android:paddingBottom="20dp" android:background="@drawable/shape_squircle_white_background"> + + + + + + \ 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 b9e719745..fc7f8ec91 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -267,7 +267,13 @@ Vibrate while incoming call is ringing Automatically start recording calls Conversations + Auto-download files + Export media in native gallery + Media from ephemeral messages will never be exported Meetings + Default layout + Active speaker + Mosaic Network Use only Wi-Fi networks User interface