Added conversations & meetings settings

This commit is contained in:
Sylvain Berfini 2024-01-10 16:20:57 +01:00
parent 6c03a6fb7a
commit 281b44a240
8 changed files with 324 additions and 138 deletions

View file

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

View file

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

View file

@ -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<Boolean>()
val autoDownloadEnabled = MutableLiveData<Boolean>()
val exportMediaEnabled = MutableLiveData<Boolean>()
// Meetings settings
val showMeetingsSettings = MutableLiveData<Boolean>()
val defaultLayout = MutableLiveData<Int>()
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<Boolean>()
// User Interface settings
val theme = MutableLiveData<Int>()
val availableThemesNames = arrayListOf<String>()
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

View file

@ -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 ""
}
}

View file

@ -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 ""
}
}
}

View file

@ -16,6 +16,76 @@
android:paddingBottom="20dp"
android:background="@drawable/shape_squircle_white_background">
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:onClick="@{() -> viewModel.toggleAutoDownload()}"
android:id="@+id/auto_download_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/settings_conversations_auto_download_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="@id/auto_download_switch"
app:layout_constraintBottom_toBottomOf="@id/auto_download_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/auto_download_switch"/>
<com.google.android.material.materialswitch.MaterialSwitch
style="@style/material_switch_style"
android:id="@+id/auto_download_switch"
android:onClick="@{() -> viewModel.toggleAutoDownload()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:checked="@{viewModel.autoDownloadEnabled}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:onClick="@{() -> viewModel.toggleExportMedia()}"
android:id="@+id/export_media_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/settings_conversations_export_media_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="@id/export_media_switch"
app:layout_constraintBottom_toTopOf="@id/export_media_subtitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/export_media_switch"/>
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_subtitle_style"
android:onClick="@{() -> viewModel.toggleExportMedia()}"
android:id="@+id/export_media_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/settings_conversations_export_media_subtitle"
app:layout_constraintTop_toBottomOf="@id/export_media_title"
app:layout_constraintBottom_toBottomOf="@id/export_media_switch"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/export_media_switch"/>
<com.google.android.material.materialswitch.MaterialSwitch
style="@style/material_switch_style"
android:id="@+id/export_media_switch"
android:onClick="@{() -> viewModel.toggleExportMedia()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp"
android:checked="@{viewModel.exportMediaEnabled}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/auto_download_switch" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -16,6 +16,44 @@
android:paddingBottom="20dp"
android:background="@drawable/shape_squircle_white_background">
<androidx.appcompat.widget.AppCompatTextView
style="@style/settings_title_style"
android:id="@+id/layout_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="10dp"
android:text="@string/settings_meetings_default_layout_title"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.appcompat.widget.AppCompatSpinner
style="@style/material_switch_style"
android:id="@+id/layout_spinner"
android:layout_width="0dp"
android:layout_height="50dp"
android:layout_marginEnd="10dp"
android:background="@drawable/edit_text_background"
android:paddingStart="20dp"
android:paddingEnd="20dp"
app:layout_constraintTop_toBottomOf="@id/layout_title"
app:layout_constraintStart_toStartOf="@id/layout_title"
app:layout_constraintEnd_toEndOf="@id/layout_title" />
<ImageView
android:id="@+id/layout_spinner_caret"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:src="@drawable/caret_down"
app:layout_constraintTop_toTopOf="@id/layout_spinner"
app:layout_constraintBottom_toBottomOf="@id/layout_spinner"
app:layout_constraintEnd_toEndOf="@id/layout_spinner"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -267,7 +267,13 @@
<string name="settings_calls_vibrate_while_ringing_title">Vibrate while incoming call is ringing</string>
<string name="settings_calls_auto_record_title">Automatically start recording calls</string>
<string name="settings_conversations_title">Conversations</string>
<string name="settings_conversations_auto_download_title">Auto-download files</string>
<string name="settings_conversations_export_media_title">Export media in native gallery</string>
<string name="settings_conversations_export_media_subtitle">Media from ephemeral messages will never be exported</string>
<string name="settings_meetings_title">Meetings</string>
<string name="settings_meetings_default_layout_title">Default layout</string>
<string name="settings_meetings_layout_active_speaker_label">Active speaker</string>
<string name="settings_meetings_layout_mosaic_label">Mosaic</string>
<string name="settings_network_title">Network</string>
<string name="settings_network_use_wifi_only">Use only Wi-Fi networks</string>
<string name="settings_user_interface_title">User interface</string>