diff --git a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt
index 994db7bea..d6cc86cb2 100644
--- a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt
@@ -28,6 +28,7 @@ import android.content.Intent
import android.graphics.RenderEffect
import android.graphics.Shader
import android.os.Build
+import android.os.Environment
import android.view.View
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
@@ -92,5 +93,9 @@ class Api31Compatibility {
Log.e("$TAG Can't start service as foreground! $e")
}
}
+
+ fun getRecordingsDirectory(): String {
+ return Environment.DIRECTORY_RECORDINGS
+ }
}
}
diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt
index c293bacf7..cecba28ac 100644
--- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt
+++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt
@@ -27,6 +27,7 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
+import android.os.Environment
import android.view.View
import androidx.appcompat.app.AppCompatDelegate
import org.linphone.core.tools.Log
@@ -174,5 +175,12 @@ class Compatibility {
)
}
}
+
+ fun getRecordingsDirectory(): String {
+ if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) {
+ return Api31Compatibility.getRecordingsDirectory()
+ }
+ return Environment.DIRECTORY_PODCASTS
+ }
}
}
diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt
index 02c2b25e5..7b8b02788 100644
--- a/app/src/main/java/org/linphone/core/CorePreferences.kt
+++ b/app/src/main/java/org/linphone/core/CorePreferences.kt
@@ -186,7 +186,7 @@ class CorePreferences @UiThread constructor(private val context: Context) {
@get:WorkerThread
val disableCallRecordings: Boolean
- get() = config.getBool("ui", "disable_call_recordings_feature", true) // TODO FIXME: not implemented yet
+ get() = config.getBool("ui", "disable_call_recordings_feature", false)
@get:WorkerThread
val oneAccountMax: Boolean
diff --git a/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt b/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt
index 665ebf2ae..4ce286347 100644
--- a/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt
+++ b/app/src/main/java/org/linphone/ui/main/contacts/fragment/EditContactFragment.kt
@@ -74,7 +74,7 @@ class EditContactFragment : SlidingPaneChildFragment() {
Log.i("$TAG Picture picked [$uri]")
val localFileName = FileUtils.getFileStoragePath(
viewModel.getPictureFileName(),
- true,
+ isImage = true,
overrideExisting = true
)
lifecycleScope.launch {
diff --git a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt
index 3911579d0..47342b2b4 100644
--- a/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/main/contacts/viewmodel/ContactNewOrEditViewModel.kt
@@ -180,7 +180,7 @@ class ContactNewOrEditViewModel @UiThread constructor() : GenericViewModel() {
if (picture.contains(TEMP_PICTURE_NAME)) {
val newFile = FileUtils.getFileStoragePath(
getPictureFileName(),
- true,
+ isImage = true,
overrideExisting = true
)
val oldFile = Uri.parse(FileUtils.getProperFilePath(picture))
diff --git a/app/src/main/java/org/linphone/ui/main/recordings/RecordingsFragment.kt b/app/src/main/java/org/linphone/ui/main/recordings/RecordingsFragment.kt
deleted file mode 100644
index 4d5e6f8cd..000000000
--- a/app/src/main/java/org/linphone/ui/main/recordings/RecordingsFragment.kt
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (c) 2010-2023 Belledonne Communications SARL.
- *
- * This file is part of linphone-android
- * (see https://www.linphone.org).
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.linphone.ui.main.recordings
-
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.annotation.UiThread
-import org.linphone.databinding.RecordingsFragmentBinding
-import org.linphone.ui.main.fragment.GenericMainFragment
-
-@UiThread
-class RecordingsFragment : GenericMainFragment() {
- private lateinit var binding: RecordingsFragmentBinding
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?
- ): View {
- binding = RecordingsFragmentBinding.inflate(layoutInflater)
- return binding.root
- }
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
- binding.lifecycleOwner = viewLifecycleOwner
-
- binding.setBackClickListener {
- goBack()
- }
- }
-}
diff --git a/app/src/main/java/org/linphone/ui/main/recordings/adapter/RecordingsListAdapter.kt b/app/src/main/java/org/linphone/ui/main/recordings/adapter/RecordingsListAdapter.kt
new file mode 100644
index 000000000..e30343d7d
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/recordings/adapter/RecordingsListAdapter.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2010-2023 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.ui.main.recordings.adapter
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.UiThread
+import androidx.databinding.DataBindingUtil
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.findViewTreeLifecycleOwner
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import org.linphone.R
+import org.linphone.databinding.RecordingListCellBinding
+import org.linphone.databinding.RecordingsListDecorationBinding
+import org.linphone.ui.main.recordings.model.RecordingModel
+import org.linphone.utils.Event
+import org.linphone.utils.HeaderAdapter
+
+class RecordingsListAdapter :
+ ListAdapter(
+ RecordingDiffCallback()
+ ),
+ HeaderAdapter {
+ var selectedAdapterPosition = -1
+
+ val recordingLongClickedEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ override fun displayHeaderForPosition(position: Int): Boolean {
+ if (position == 0) return true
+
+ val previous = getItem(position - 1)
+ val item = getItem(position)
+ return previous.month != item.month
+ }
+
+ override fun getHeaderViewForPosition(context: Context, position: Int): View {
+ val binding = RecordingsListDecorationBinding.inflate(LayoutInflater.from(context))
+ val item = getItem(position)
+ binding.header.text = item.month
+ return binding.root
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
+ val binding: RecordingListCellBinding = DataBindingUtil.inflate(
+ LayoutInflater.from(parent.context),
+ R.layout.recording_list_cell,
+ parent,
+ false
+ )
+ val viewHolder = ViewHolder(binding)
+ binding.apply {
+ lifecycleOwner = parent.findViewTreeLifecycleOwner()
+
+ setOnLongClickListener {
+ selectedAdapterPosition = viewHolder.bindingAdapterPosition
+ root.isSelected = true
+ recordingLongClickedEvent.value = Event(model!!)
+ true
+ }
+ }
+ return viewHolder
+ }
+
+ override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
+ (holder as ViewHolder).bind(getItem(position))
+ }
+
+ fun resetSelection() {
+ notifyItemChanged(selectedAdapterPosition)
+ selectedAdapterPosition = -1
+ }
+
+ inner class ViewHolder(
+ val binding: RecordingListCellBinding
+ ) : RecyclerView.ViewHolder(binding.root) {
+ @UiThread
+ fun bind(recordingModel: RecordingModel) {
+ with(binding) {
+ model = recordingModel
+
+ binding.root.isSelected = bindingAdapterPosition == selectedAdapterPosition
+
+ executePendingBindings()
+ }
+ }
+ }
+
+ private class RecordingDiffCallback : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: RecordingModel, newItem: RecordingModel): Boolean {
+ return false
+ }
+
+ override fun areContentsTheSame(oldItem: RecordingModel, newItem: RecordingModel): Boolean {
+ return false
+ }
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsFragment.kt b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsFragment.kt
new file mode 100644
index 000000000..6b0110be8
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsFragment.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2010-2023 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.ui.main.recordings.fragment
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.UiThread
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.LinearLayoutManager
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import kotlinx.coroutines.launch
+import org.linphone.core.tools.Log
+import org.linphone.databinding.RecordingsFragmentBinding
+import org.linphone.ui.main.fragment.GenericMainFragment
+import org.linphone.ui.main.recordings.adapter.RecordingsListAdapter
+import org.linphone.ui.main.recordings.viewmodel.RecordingsListViewModel
+import org.linphone.utils.RecyclerViewHeaderDecoration
+import org.linphone.utils.hideKeyboard
+import org.linphone.utils.showKeyboard
+
+@UiThread
+class RecordingsFragment : GenericMainFragment() {
+ companion object {
+ private const val TAG = "[Recordings List Fragment]"
+ }
+
+ private lateinit var binding: RecordingsFragmentBinding
+
+ private lateinit var listViewModel: RecordingsListViewModel
+
+ private lateinit var adapter: RecordingsListAdapter
+
+ private var bottomSheetDialog: BottomSheetDialogFragment? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ adapter = RecordingsListAdapter()
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ binding = RecordingsFragmentBinding.inflate(layoutInflater)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ listViewModel = ViewModelProvider(this)[RecordingsListViewModel::class.java]
+
+ binding.lifecycleOwner = viewLifecycleOwner
+ binding.viewModel = listViewModel
+
+ binding.setBackClickListener {
+ goBack()
+ }
+
+ binding.recordingsList.setHasFixedSize(true)
+ binding.recordingsList.layoutManager = LinearLayoutManager(requireContext())
+ val headerItemDecoration = RecyclerViewHeaderDecoration(requireContext(), adapter)
+ binding.recordingsList.addItemDecoration(headerItemDecoration)
+
+ if (binding.recordingsList.adapter != adapter) {
+ binding.recordingsList.adapter = adapter
+ }
+
+ listViewModel.recordings.observe(viewLifecycleOwner) {
+ val count = it.size
+ adapter.submitList(it)
+ Log.i("$TAG Recordings list ready with [$count] items")
+ listViewModel.fetchInProgress.value = false
+ }
+
+ listViewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
+ listViewModel.applyFilter(filter.trim())
+ }
+
+ listViewModel.focusSearchBarEvent.observe(viewLifecycleOwner) {
+ it.consume { show ->
+ if (show) {
+ // To automatically open keyboard
+ binding.search.showKeyboard()
+ } else {
+ binding.search.hideKeyboard()
+ }
+ }
+ }
+
+ adapter.recordingLongClickedEvent.observe(viewLifecycleOwner) {
+ it.consume { model ->
+ val modalBottomSheet = RecordingsMenuDialogFragment(
+ { // onDismiss
+ adapter.resetSelection()
+ },
+ { // onShare
+ adapter.resetSelection()
+ },
+ { // onExport
+ adapter.resetSelection()
+ },
+ { // onDelete
+ Log.i("$TAG Deleting meeting [${model.filePath}]")
+ lifecycleScope.launch {
+ model.delete()
+ }
+ listViewModel.applyFilter(listViewModel.searchFilter.value.orEmpty())
+ }
+ )
+ modalBottomSheet.show(parentFragmentManager, RecordingsMenuDialogFragment.TAG)
+ bottomSheetDialog = modalBottomSheet
+ }
+ }
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ bottomSheetDialog?.dismiss()
+ bottomSheetDialog = null
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsMenuDialogFragment.kt b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsMenuDialogFragment.kt
new file mode 100644
index 000000000..f358d687f
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/recordings/fragment/RecordingsMenuDialogFragment.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2010-2023 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.ui.main.recordings.fragment
+
+import android.app.Dialog
+import android.content.DialogInterface
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.UiThread
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.bottomsheet.BottomSheetDialog
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
+import org.linphone.databinding.RecordingsListLongPressMenuBinding
+
+@UiThread
+class RecordingsMenuDialogFragment(
+ private val onDismiss: (() -> Unit)? = null,
+ private val onShare: (() -> Unit)? = null,
+ private val onExport: (() -> Unit)? = null,
+ private val onDelete: (() -> Unit)? = null
+) : BottomSheetDialogFragment() {
+ companion object {
+ const val TAG = "RecordingsMenuDialogFragment"
+ }
+
+ override fun onCancel(dialog: DialogInterface) {
+ onDismiss?.invoke()
+ super.onCancel(dialog)
+ }
+
+ override fun onDismiss(dialog: DialogInterface) {
+ onDismiss?.invoke()
+ super.onDismiss(dialog)
+ }
+
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
+ // Makes sure all menu entries are visible,
+ // required for landscape mode (otherwise only first item is visible)
+ dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED
+ return dialog
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View {
+ val view = RecordingsListLongPressMenuBinding.inflate(layoutInflater)
+
+ view.setDeleteClickListener {
+ onDelete?.invoke()
+ dismiss()
+ }
+
+ return view.root
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt
new file mode 100644
index 000000000..e43d9284c
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/recordings/model/RecordingModel.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (c) 2010-2024 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.ui.main.recordings.model
+
+import androidx.annotation.UiThread
+import androidx.annotation.WorkerThread
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.Factory
+import org.linphone.core.tools.Log
+import org.linphone.utils.FileUtils
+import org.linphone.utils.LinphoneUtils
+import org.linphone.utils.TimestampUtils
+
+class RecordingModel @WorkerThread constructor(val filePath: String, val fileName: String) {
+ companion object {
+ private const val TAG = "[Recording Model]"
+ }
+
+ val displayName: String
+
+ val month: String
+
+ val dateTime: String
+
+ init {
+ val withoutHeader = fileName.substring(LinphoneUtils.RECORDING_FILE_NAME_HEADER.length)
+ val indexOfSeparator = withoutHeader.indexOf(
+ LinphoneUtils.RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR
+ )
+ val sipUri = withoutHeader.substring(0, indexOfSeparator)
+ val timestamp = withoutHeader.substring(
+ indexOfSeparator + LinphoneUtils.RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR.length,
+ withoutHeader.length - LinphoneUtils.RECORDING_FILE_EXTENSION.length
+ )
+ Log.i("$TAG Extract SIP URI [$sipUri] and timestamp [$timestamp] from file [$fileName]")
+
+ val parsedTimestamp = timestamp.toLong()
+ month = TimestampUtils.month(parsedTimestamp, timestampInSecs = false)
+ val date = TimestampUtils.toString(
+ parsedTimestamp,
+ timestampInSecs = false,
+ onlyDate = true,
+ shortDate = false
+ )
+ val time = TimestampUtils.timeToString(parsedTimestamp, timestampInSecs = false)
+ dateTime = "$date - $time"
+
+ val sipAddress = Factory.instance().createAddress(sipUri)
+ displayName = if (sipAddress != null) {
+ val contact = coreContext.contactsManager.findContactByAddress(sipAddress)
+ contact?.name ?: LinphoneUtils.getDisplayName(sipAddress)
+ } else {
+ sipUri
+ }
+ }
+
+ @UiThread
+ suspend fun delete() {
+ Log.i("$TAG Deleting call recording [$filePath]")
+ FileUtils.deleteFile(filePath)
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt
new file mode 100644
index 000000000..c177079c2
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/main/recordings/viewmodel/RecordingsListViewModel.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2010-2024 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.ui.main.recordings.viewmodel
+
+import androidx.annotation.UiThread
+import androidx.annotation.WorkerThread
+import androidx.lifecycle.MutableLiveData
+import org.linphone.LinphoneApplication.Companion.coreContext
+import org.linphone.core.tools.Log
+import org.linphone.ui.GenericViewModel
+import org.linphone.ui.main.recordings.model.RecordingModel
+import org.linphone.utils.Event
+import org.linphone.utils.FileUtils
+
+class RecordingsListViewModel @UiThread constructor() : GenericViewModel() {
+ companion object {
+ private const val TAG = "[Recordings List ViewModel]"
+ }
+
+ val recordings = MutableLiveData>()
+
+ val searchBarVisible = MutableLiveData()
+
+ val searchFilter = MutableLiveData()
+
+ val fetchInProgress = MutableLiveData()
+
+ val focusSearchBarEvent: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+
+ init {
+ searchBarVisible.value = false
+ fetchInProgress.value = true
+
+ coreContext.postOnCoreThread {
+ computeList("")
+ }
+ }
+
+ @UiThread
+ fun openSearchBar() {
+ searchBarVisible.value = true
+ focusSearchBarEvent.value = Event(true)
+ }
+
+ @UiThread
+ fun closeSearchBar() {
+ clearFilter()
+ searchBarVisible.value = false
+ focusSearchBarEvent.value = Event(false)
+ }
+
+ @UiThread
+ fun clearFilter() {
+ searchFilter.value = ""
+ }
+
+ @UiThread
+ fun applyFilter(filter: String) {
+ coreContext.postOnCoreThread {
+ computeList(filter)
+ }
+ }
+
+ @WorkerThread
+ private fun computeList(filter: String) {
+ // TODO FIXME: use filter
+ val list = arrayListOf()
+
+ // TODO FIXME: also load recordings from previous Linphone versions
+ for (file in FileUtils.getFileStorageDir(isRecording = true).listFiles().orEmpty()) {
+ val path = file.path
+ val name = file.name
+ Log.i("$TAG Found file $path")
+ list.add(RecordingModel(path, name))
+ }
+
+ list.sortBy {
+ it.filePath // TODO FIXME
+ }
+ recordings.postValue(list)
+ }
+}
diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt
index 52f97b0ce..ecf94d15c 100644
--- a/app/src/main/java/org/linphone/utils/FileUtils.kt
+++ b/app/src/main/java/org/linphone/utils/FileUtils.kt
@@ -146,9 +146,10 @@ class FileUtils {
fun getFileStoragePath(
fileName: String,
isImage: Boolean = false,
+ isRecording: Boolean = false,
overrideExisting: Boolean = false
): File {
- val path = getFileStorageDir(isImage)
+ val path = getFileStorageDir(isPicture = isImage, isRecording = isRecording)
var file = File(path, fileName)
if (!overrideExisting) {
@@ -246,7 +247,11 @@ class FileUtils {
val localFile: File = if (copyToCache) {
getFileStorageCacheDir(name, overrideExisting)
} else {
- getFileStoragePath(name, isImage, overrideExisting)
+ getFileStoragePath(
+ name,
+ isImage = isImage,
+ overrideExisting = overrideExisting
+ )
}
copyFile(uri, localFile)
return@withContext localFile.absolutePath
@@ -455,7 +460,7 @@ class FileUtils {
}
@AnyThread
- private fun getFileStorageDir(isPicture: Boolean = false): File {
+ fun getFileStorageDir(isPicture: Boolean = false, isRecording: Boolean = false): File {
var path: File? = null
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
Log.w("$TAG External storage is mounted")
@@ -463,6 +468,9 @@ class FileUtils {
if (isPicture) {
Log.w("$TAG Using pictures directory instead of downloads")
directory = Environment.DIRECTORY_PICTURES
+ } else if (isRecording) {
+ directory = Compatibility.getRecordingsDirectory()
+ Log.w("$TAG Using [$directory] directory instead of downloads")
}
path = coreContext.context.getExternalFilesDir(directory)
}
diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt
index a9501f129..288420a19 100644
--- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt
+++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt
@@ -23,10 +23,6 @@ import androidx.annotation.AnyThread
import androidx.annotation.DrawableRes
import androidx.annotation.IntegerRes
import androidx.annotation.WorkerThread
-import java.text.DateFormat
-import java.text.SimpleDateFormat
-import java.util.Date
-import java.util.Locale
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.R
import org.linphone.core.Account
@@ -48,7 +44,10 @@ class LinphoneUtils {
companion object {
private const val TAG = "[Linphone Utils]"
- private const val RECORDING_DATE_PATTERN = "dd-MM-yyyy-HH-mm-ss"
+ const val RECORDING_FILE_NAME_HEADER = "call_recording_"
+ const val RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR = "_on_"
+ const val RECORDING_FILE_EXTENSION = ".mkv"
+
private const val CHAT_ROOM_ID_SEPARATOR = "#~#"
@WorkerThread
@@ -319,13 +318,8 @@ class LinphoneUtils {
@WorkerThread
fun getRecordingFilePathForAddress(address: Address): String {
- val displayName = getDisplayName(address)
- val dateFormat: DateFormat = SimpleDateFormat(
- RECORDING_DATE_PATTERN,
- Locale.getDefault()
- )
- val fileName = "${displayName}_${dateFormat.format(Date())}.mkv"
- return FileUtils.getFileStoragePath(fileName).absolutePath
+ val fileName = "${RECORDING_FILE_NAME_HEADER}${address.asStringUriOnly()}${RECORDING_FILE_NAME_URI_TIMESTAMP_SEPARATOR}${System.currentTimeMillis()}$RECORDING_FILE_EXTENSION"
+ return FileUtils.getFileStoragePath(fileName, isRecording = true).absolutePath
}
@WorkerThread
diff --git a/app/src/main/res/drawable/microphone_stage.xml b/app/src/main/res/drawable/microphone_stage.xml
deleted file mode 100644
index fe79e7532..000000000
--- a/app/src/main/res/drawable/microphone_stage.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml
index 33a8b500e..8d9a68711 100644
--- a/app/src/main/res/layout/chat_conversation_fragment.xml
+++ b/app/src/main/res/layout/chat_conversation_fragment.xml
@@ -58,13 +58,6 @@
app:constraint_referenced_ids="cancel_search, search, clear_field"
android:visibility="@{viewModel.searchBarVisible ? View.VISIBLE : View.GONE, default=gone}" />
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/recordings_fragment.xml b/app/src/main/res/layout/recordings_fragment.xml
index 723d38731..30fd41b6a 100644
--- a/app/src/main/res/layout/recordings_fragment.xml
+++ b/app/src/main/res/layout/recordings_fragment.xml
@@ -8,6 +8,9 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/recordings_list_decoration.xml b/app/src/main/res/layout/recordings_list_decoration.xml
new file mode 100644
index 000000000..4b9aa7512
--- /dev/null
+++ b/app/src/main/res/layout/recordings_list_decoration.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/recordings_list_long_press_menu.xml b/app/src/main/res/layout/recordings_list_long_press_menu.xml
new file mode 100644
index 000000000..931b4cf63
--- /dev/null
+++ b/app/src/main/res/layout/recordings_list_long_press_menu.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/main_nav_graph.xml b/app/src/main/res/navigation/main_nav_graph.xml
index b44018cc9..38c275d68 100644
--- a/app/src/main/res/navigation/main_nav_graph.xml
+++ b/app/src/main/res/navigation/main_nav_graph.xml
@@ -174,7 +174,7 @@