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