From 751e19e7a54ee3e2f123be57164f397504a71290 Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Fri, 28 Jul 2023 22:30:07 +0530 Subject: [PATCH 1/5] Bug fixes and performance improvements --- app/src/main/java/com/bnyro/recorder/App.kt | 2 + .../java/com/bnyro/recorder/AppContainer.kt | 11 +++ .../com/bnyro/recorder/enums/SortOrder.kt | 1 + .../recorder/ui/components/PlayerView.kt | 36 +------ .../recorder/ui/components/RecordingItem.kt | 8 +- .../ui/components/RecordingItemList.kt | 14 ++- .../bnyro/recorder/ui/models/PlayerModel.kt | 94 ++++++++----------- .../bnyro/recorder/ui/screens/PlayerScreen.kt | 59 ++++++------ .../com/bnyro/recorder/util/FileRepository.kt | 72 ++++++++++++++ app/src/main/res/values/strings.xml | 1 + 10 files changed, 167 insertions(+), 131 deletions(-) create mode 100644 app/src/main/java/com/bnyro/recorder/AppContainer.kt create mode 100644 app/src/main/java/com/bnyro/recorder/util/FileRepository.kt diff --git a/app/src/main/java/com/bnyro/recorder/App.kt b/app/src/main/java/com/bnyro/recorder/App.kt index 9ee36a78..d3524dd7 100644 --- a/app/src/main/java/com/bnyro/recorder/App.kt +++ b/app/src/main/java/com/bnyro/recorder/App.kt @@ -6,10 +6,12 @@ import com.bnyro.recorder.util.Preferences import com.bnyro.recorder.util.ShortcutHelper class App : Application() { + lateinit var container: AppContainer override fun onCreate() { super.onCreate() Preferences.init(this) NotificationHelper.buildNotificationChannels(this) ShortcutHelper.createShortcuts(this) + container = AppContainer(this) } } diff --git a/app/src/main/java/com/bnyro/recorder/AppContainer.kt b/app/src/main/java/com/bnyro/recorder/AppContainer.kt new file mode 100644 index 00000000..e3575d64 --- /dev/null +++ b/app/src/main/java/com/bnyro/recorder/AppContainer.kt @@ -0,0 +1,11 @@ +package com.bnyro.recorder + +import android.content.Context +import com.bnyro.recorder.util.FileRepository +import com.bnyro.recorder.util.FileRepositoryImpl + +class AppContainer(context: Context) { + val fileRepository: FileRepository by lazy { + FileRepositoryImpl(context) + } +} diff --git a/app/src/main/java/com/bnyro/recorder/enums/SortOrder.kt b/app/src/main/java/com/bnyro/recorder/enums/SortOrder.kt index d5ae5933..e7dea0ae 100644 --- a/app/src/main/java/com/bnyro/recorder/enums/SortOrder.kt +++ b/app/src/main/java/com/bnyro/recorder/enums/SortOrder.kt @@ -1,6 +1,7 @@ package com.bnyro.recorder.enums enum class SortOrder { + DEFAULT, ALPHABETIC, ALPHABETIC_REV, SIZE, diff --git a/app/src/main/java/com/bnyro/recorder/ui/components/PlayerView.kt b/app/src/main/java/com/bnyro/recorder/ui/components/PlayerView.kt index b1cb29bb..d4d73771 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/components/PlayerView.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/components/PlayerView.kt @@ -13,30 +13,19 @@ import androidx.compose.material3.TabRow import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.bnyro.recorder.R -import com.bnyro.recorder.obj.RecordingItemData -import com.bnyro.recorder.ui.dialogs.ConfirmationDialog import com.bnyro.recorder.ui.models.PlayerModel import kotlinx.coroutines.launch @OptIn(ExperimentalFoundationApi::class) @Composable fun PlayerView( - showVideoModeInitially: Boolean, - showDeleteDialog: Boolean, - selectedFiles: MutableState>, - onDeleteAllDialogDismissed: () -> Unit + showVideoModeInitially: Boolean ) { - val playerModel: PlayerModel = viewModel() - val context = LocalContext.current - - LaunchedEffect(Unit) { - playerModel.loadFiles(context) - } + val playerModel: PlayerModel = viewModel(factory = PlayerModel.Factory) Column( modifier = Modifier.fillMaxSize() @@ -85,32 +74,15 @@ fun PlayerView( ) { index -> when (index) { 0 -> RecordingItemList( - items = playerModel.recordingItems.filter { it.isAudio }, - selectedFiles, + items = playerModel.audioRecordingItems, isVideoList = false ) 1 -> RecordingItemList( - items = playerModel.recordingItems.filter { it.isVideo }, - selectedFiles, + items = playerModel.screenRecordingItems, isVideoList = true ) } } } - - if (showDeleteDialog) { - ConfirmationDialog( - title = if (selectedFiles.value.isEmpty()) R.string.delete_all else R.string.delete, - onDismissRequest = onDeleteAllDialogDismissed - ) { - val filesToDelete = - selectedFiles.value.takeIf { it.isNotEmpty() } ?: playerModel.recordingItems - filesToDelete.forEach { - if (it.recordingFile.exists()) it.recordingFile.delete() - playerModel.recordingItems.remove(it) - selectedFiles.value -= it - } - } - } } diff --git a/app/src/main/java/com/bnyro/recorder/ui/components/RecordingItem.kt b/app/src/main/java/com/bnyro/recorder/ui/components/RecordingItem.kt index 2b137a0f..e3fa81e5 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/components/RecordingItem.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/components/RecordingItem.kt @@ -47,7 +47,7 @@ fun RecordingItem( onClick: (wasLongClick: Boolean) -> Unit, startPlayingAudio: () -> Unit ) { - val playerModel: PlayerModel = viewModel() + val playerModel: PlayerModel = viewModel(factory = PlayerModel.Factory) val context = LocalContext.current var showRenameDialog by remember { @@ -207,9 +207,7 @@ fun RecordingItem( confirmButton = { DialogButton(stringResource(R.string.okay)) { recordingFile.renameTo(fileName) - val index = playerModel.files.indexOf(recordingFile) - playerModel.files.removeAt(index) - playerModel.files.add(index, recordingFile) + playerModel.loadFiles() showRenameDialog = false } }, @@ -228,7 +226,7 @@ fun RecordingItem( ) { playerModel.stopPlaying() recordingFile.delete() - playerModel.files.remove(recordingFile) + playerModel.loadFiles() } } diff --git a/app/src/main/java/com/bnyro/recorder/ui/components/RecordingItemList.kt b/app/src/main/java/com/bnyro/recorder/ui/components/RecordingItemList.kt index f43b924f..f266b0b4 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/components/RecordingItemList.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/components/RecordingItemList.kt @@ -16,7 +16,6 @@ import androidx.compose.material.icons.filled.VideoFile import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -30,7 +29,6 @@ import com.bnyro.recorder.ui.models.PlayerModel @Composable fun RecordingItemList( items: List, - selectedFiles: MutableState>, isVideoList: Boolean, playerModel: PlayerModel = viewModel() ) { @@ -46,15 +44,15 @@ fun RecordingItemList( items(items) { RecordingItem( it, - isSelected = selectedFiles.value.contains(it), + isSelected = playerModel.selectedFiles.contains(it), onClick = { wasLongPress -> when { - wasLongPress -> selectedFiles.value += it - selectedFiles.value.isNotEmpty() -> { - if (selectedFiles.value.contains(it)) { - selectedFiles.value -= it + wasLongPress -> playerModel.selectedFiles += it + playerModel.selectedFiles.isNotEmpty() -> { + if (playerModel.selectedFiles.contains(it)) { + playerModel.selectedFiles -= it } else { - selectedFiles.value += it + playerModel.selectedFiles += it } } } diff --git a/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt b/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt index 4ded9ac5..13479085 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt @@ -1,85 +1,64 @@ package com.bnyro.recorder.ui.models import android.content.Context -import android.media.MediaMetadataRetriever import android.media.MediaPlayer -import android.provider.OpenableColumns import android.util.Log import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.runtime.toMutableStateList import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewModelScope -import com.bnyro.recorder.enums.RecorderType +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import com.bnyro.recorder.App import com.bnyro.recorder.enums.SortOrder import com.bnyro.recorder.obj.RecordingItemData +import com.bnyro.recorder.util.FileRepository import com.bnyro.recorder.util.PlayerHelper -import com.bnyro.recorder.util.StorageHelper import java.io.IOException -import kotlin.math.absoluteValue -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -class PlayerModel : ViewModel() { +class PlayerModel(private val fileRepository: FileRepository) : ViewModel() { var isPlaying by mutableStateOf(false) var player by mutableStateOf(null) var currentPlayingFile by mutableStateOf(null) - var files = mutableStateListOf() + var selectedFiles by mutableStateOf(listOf()) - val recordingItems = mutableStateListOf() + private var sortOrder = SortOrder.DEFAULT - fun loadFiles(context: Context) { + var audioRecordingItems by mutableStateOf(listOf()) + var screenRecordingItems by mutableStateOf(listOf()) + + init { + loadFiles() + } + + fun loadFiles() { viewModelScope.launch { - withContext(Dispatchers.IO) { - files = getAvailableFiles(context).toMutableStateList() - } - loadRecordingItems(context) + audioRecordingItems = fileRepository.getAudioRecordingItems(sortOrder) + screenRecordingItems = fileRepository.getVideoRecordingItems(sortOrder) } } - fun sortRecordingItems(context: Context, sortOrder: SortOrder) { - files = when (sortOrder) { - SortOrder.ALPHABETIC -> files.sortedBy { it.name } - SortOrder.ALPHABETIC_REV -> files.sortedByDescending { it.name } - SortOrder.SIZE_REV -> files.sortedBy { - context.contentResolver.query(it.uri, null, null, null, null)?.use { cursor -> - cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE).absoluteValue) - } - } - - SortOrder.SIZE -> files.sortedByDescending { - context.contentResolver.query(it.uri, null, null, null, null)?.use { cursor -> - cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE).absoluteValue) - } - } - }.toMutableStateList() - loadRecordingItems(context) + fun sortItems(newSortOrder: SortOrder) { + if (newSortOrder == sortOrder) return + sortOrder = newSortOrder + loadFiles() } - private fun loadRecordingItems(context: Context) { + fun deleteFiles() { viewModelScope.launch { - recordingItems.clear() - files.forEach { file -> - if (file.name.orEmpty() - .endsWith("mp4" /* Currently there are only mp4 video files*/) - ) { - val thumbnail = - MediaMetadataRetriever().apply { - setDataSource( - context, - file.uri - ) - }.frameAtTime - recordingItems.add(RecordingItemData(file, RecorderType.VIDEO, thumbnail)) - } else { - recordingItems.add(RecordingItemData(file, RecorderType.AUDIO)) - } + if (selectedFiles.isEmpty()) { + fileRepository.deleteAllFiles() + loadFiles() + return@launch } + fileRepository.deleteSelectedFiles(selectedFiles.map { it.recordingFile }) + loadFiles() } } @@ -121,13 +100,18 @@ class PlayerModel : ViewModel() { isPlaying = true } - private fun getAvailableFiles(context: Context): List { - return StorageHelper.getOutputDir(context).listFiles().filter { it.isFile }.toList() - } - private fun getMediaPlayer(): MediaPlayer { return MediaPlayer().apply { setAudioAttributes(PlayerHelper.getAudioAttributes()) } } + + companion object { + val Factory: ViewModelProvider.Factory = viewModelFactory { + initializer { + val application = (this[APPLICATION_KEY] as App) + PlayerModel(application.container.fileRepository) + } + } + } } diff --git a/app/src/main/java/com/bnyro/recorder/ui/screens/PlayerScreen.kt b/app/src/main/java/com/bnyro/recorder/ui/screens/PlayerScreen.kt index 7cb6f5c3..d957ce3d 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/screens/PlayerScreen.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/screens/PlayerScreen.kt @@ -20,15 +20,14 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.bnyro.recorder.R import com.bnyro.recorder.enums.SortOrder -import com.bnyro.recorder.obj.RecordingItemData import com.bnyro.recorder.ui.common.ClickableIcon import com.bnyro.recorder.ui.components.PlayerView +import com.bnyro.recorder.ui.dialogs.ConfirmationDialog import com.bnyro.recorder.ui.models.PlayerModel @OptIn(ExperimentalMaterial3Api::class) @@ -42,10 +41,7 @@ fun PlayerScreen( var selectedSortOrder by remember { mutableStateOf(SortOrder.ALPHABETIC) } - val selectedFiles = remember { - mutableStateOf(listOf()) - } - val playerModel: PlayerModel = viewModel() + val playerModel: PlayerModel = viewModel(factory = PlayerModel.Factory) val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior( rememberTopAppBarState() @@ -71,43 +67,39 @@ fun PlayerScreen( } val sortOptions = listOf( - SortOrder.ALPHABETIC, - SortOrder.ALPHABETIC_REV, - SortOrder.SIZE, - SortOrder.SIZE_REV - ) - val sortOptionNames = listOf( - R.string.alphabetic, - R.string.alphabetic_rev, - R.string.size, - R.string.size_rev + SortOrder.DEFAULT to R.string.default_sort, + SortOrder.ALPHABETIC to R.string.alphabetic, + SortOrder.ALPHABETIC_REV to R.string.alphabetic_rev, + SortOrder.SIZE to R.string.size, + SortOrder.SIZE_REV to R.string.size_rev ) DropdownMenu(showDropDown, { showDropDown = false }) { - val context = LocalContext.current - sortOptions.forEachIndexed { index, sortOrder -> + sortOptions.forEach { sortOrder -> DropdownMenuItem( text = { - Text(stringResource(sortOptionNames[index])) + Text(stringResource(sortOrder.second)) }, onClick = { - selectedSortOrder = sortOrder - playerModel.sortRecordingItems(context, sortOrder) + selectedSortOrder = sortOrder.first + playerModel.sortItems(sortOrder.first) showDropDown = false } ) } } } - if (selectedFiles.value.isNotEmpty()) { - val selectedAll = selectedFiles.value.size == playerModel.files.size + if (playerModel.selectedFiles.isNotEmpty()) { + val selectedAll = + playerModel.selectedFiles.size == playerModel.audioRecordingItems.size + playerModel.screenRecordingItems.size Checkbox( modifier = Modifier.align(Alignment.CenterVertically), checked = selectedAll, onCheckedChange = { if (selectedAll) { - selectedFiles.value = listOf() + playerModel.selectedFiles = listOf() } else { - selectedFiles.value = playerModel.recordingItems + playerModel.selectedFiles = + playerModel.screenRecordingItems + playerModel.audioRecordingItems } } ) @@ -129,12 +121,17 @@ fun PlayerScreen( .padding(horizontal = 16.dp) ) { PlayerView( - showVideoModeInitially, - showDeleteDialog, - selectedFiles - ) { - showDeleteDialog = false - } + showVideoModeInitially + ) + } + } + + if (showDeleteDialog) { + ConfirmationDialog( + title = if (playerModel.selectedFiles.isEmpty()) R.string.delete_all else R.string.delete, + onDismissRequest = { showDeleteDialog = false } + ) { + playerModel.deleteFiles() } } } diff --git a/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt b/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt new file mode 100644 index 00000000..edff9ee9 --- /dev/null +++ b/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt @@ -0,0 +1,72 @@ +package com.bnyro.recorder.util + +import android.content.Context +import android.media.MediaMetadataRetriever +import androidx.documentfile.provider.DocumentFile +import com.bnyro.recorder.enums.RecorderType +import com.bnyro.recorder.enums.SortOrder +import com.bnyro.recorder.obj.RecordingItemData + +interface FileRepository { + suspend fun getVideoRecordingItems(sortOrder: SortOrder): List + suspend fun getAudioRecordingItems(sortOrder: SortOrder): List + suspend fun deleteSelectedFiles(files: List) + suspend fun deleteAllFiles() +} + +class FileRepositoryImpl(val context: Context) : FileRepository { + + private fun getVideoFiles(): List = + StorageHelper.getOutputDir(context).listFiles().filter { + it.isFile && it.name.orEmpty().endsWith("mp4") + } + + private fun getAudioFiles(): List = + StorageHelper.getOutputDir(context).listFiles().filter { + it.isFile && !it.name.orEmpty().endsWith("mp4") + } + + override suspend fun getVideoRecordingItems(sortOrder: SortOrder): List { + return getVideoFiles().sortedBy(sortOrder).map { + val thumbnail = + MediaMetadataRetriever().apply { + setDataSource( + context, + it.uri + ) + }.frameAtTime + RecordingItemData(it, RecorderType.VIDEO, thumbnail) + } + } + + override suspend fun getAudioRecordingItems(sortOrder: SortOrder): List { + return getAudioFiles().sortedBy(sortOrder).map { RecordingItemData(it, RecorderType.AUDIO) } + } + + override suspend fun deleteSelectedFiles(files: List) { + files.forEach { + if (it.exists()) it.delete() + } + } + + override suspend fun deleteAllFiles() { + StorageHelper.getOutputDir(context).listFiles().forEach { + if (it.isFile) it.delete() + } + } +} + +fun List.sortedBy(sortOrder: SortOrder): List { + return when (sortOrder) { + SortOrder.DEFAULT -> this + SortOrder.ALPHABETIC -> sortedBy { it.name } + SortOrder.ALPHABETIC_REV -> sortedByDescending { it.name } + SortOrder.SIZE_REV -> sortedBy { + it.length() + } + + SortOrder.SIZE -> sortedByDescending { + it.length() + } + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c087c35..f2ee7d96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -67,4 +67,5 @@ Screen Record Annotation Show annotation tool during screen recording Amoled Dark + Default \ No newline at end of file From 6da57797f4a24922298e904470a195c61b53a15c Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Sat, 29 Jul 2023 15:39:47 +0530 Subject: [PATCH 2/5] Remove StorageHelper in favour of FileRepository --- .../receivers/FinishedNotificationReceiver.kt | 5 +- .../recorder/services/AudioRecorderService.kt | 5 +- .../services/LosslessRecorderService.kt | 10 ++-- .../services/ScreenRecorderService.kt | 8 ++- .../ui/components/NamingPatternPref.kt | 4 +- .../com/bnyro/recorder/util/FileRepository.kt | 50 +++++++++++++++++-- .../com/bnyro/recorder/util/StorageHelper.kt | 42 ---------------- 7 files changed, 63 insertions(+), 61 deletions(-) delete mode 100644 app/src/main/java/com/bnyro/recorder/util/StorageHelper.kt diff --git a/app/src/main/java/com/bnyro/recorder/receivers/FinishedNotificationReceiver.kt b/app/src/main/java/com/bnyro/recorder/receivers/FinishedNotificationReceiver.kt index 0e0b4e95..65e029ce 100644 --- a/app/src/main/java/com/bnyro/recorder/receivers/FinishedNotificationReceiver.kt +++ b/app/src/main/java/com/bnyro/recorder/receivers/FinishedNotificationReceiver.kt @@ -4,15 +4,16 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import androidx.core.app.NotificationManagerCompat +import com.bnyro.recorder.App import com.bnyro.recorder.services.RecorderService import com.bnyro.recorder.util.IntentHelper import com.bnyro.recorder.util.NotificationHelper -import com.bnyro.recorder.util.StorageHelper class FinishedNotificationReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val fileName = intent.getStringExtra(RecorderService.FILE_NAME_EXTRA_KEY) ?: return - val file = StorageHelper.getOutputDir(context).findFile(fileName) + val file = (context.applicationContext as App).container.fileRepository + .getOutputDir().findFile(fileName) when (intent.getStringExtra(RecorderService.ACTION_EXTRA_KEY)) { RecorderService.SHARE_ACTION -> file?.let { IntentHelper.shareFile(context, it) } diff --git a/app/src/main/java/com/bnyro/recorder/services/AudioRecorderService.kt b/app/src/main/java/com/bnyro/recorder/services/AudioRecorderService.kt index ab22541a..0591e62f 100644 --- a/app/src/main/java/com/bnyro/recorder/services/AudioRecorderService.kt +++ b/app/src/main/java/com/bnyro/recorder/services/AudioRecorderService.kt @@ -1,12 +1,12 @@ package com.bnyro.recorder.services +import com.bnyro.recorder.App import com.bnyro.recorder.R import com.bnyro.recorder.enums.AudioChannels import com.bnyro.recorder.enums.AudioDeviceSource import com.bnyro.recorder.obj.AudioFormat import com.bnyro.recorder.util.PlayerHelper import com.bnyro.recorder.util.Preferences -import com.bnyro.recorder.util.StorageHelper class AudioRecorderService : RecorderService() { override val notificationTitle: String @@ -38,8 +38,7 @@ class AudioRecorderService : RecorderService() { setOutputFormat(audioFormat.format) setAudioEncoder(audioFormat.codec) - outputFile = StorageHelper.getOutputFile( - this@AudioRecorderService, + outputFile = (application as App).container.fileRepository.getOutputFile( audioFormat.extension ) fileDescriptor = contentResolver.openFileDescriptor(outputFile!!.uri, "w") diff --git a/app/src/main/java/com/bnyro/recorder/services/LosslessRecorderService.kt b/app/src/main/java/com/bnyro/recorder/services/LosslessRecorderService.kt index 2ba04f63..edaabb6d 100644 --- a/app/src/main/java/com/bnyro/recorder/services/LosslessRecorderService.kt +++ b/app/src/main/java/com/bnyro/recorder/services/LosslessRecorderService.kt @@ -7,10 +7,10 @@ import android.media.MediaRecorder import android.os.Build import androidx.annotation.RequiresApi import androidx.documentfile.provider.DocumentFile +import com.bnyro.recorder.App import com.bnyro.recorder.R import com.bnyro.recorder.enums.RecorderState import com.bnyro.recorder.util.PcmConverter -import com.bnyro.recorder.util.StorageHelper import java.io.File import kotlin.concurrent.thread import kotlin.experimental.and @@ -110,9 +110,11 @@ class LosslessRecorderService : RecorderService() { private fun convertToWav() { val inputStream = contentResolver.openInputStream(outputFile?.uri ?: return) ?: return - val outputStream = StorageHelper.getOutputFile(this, FILE_NAME_EXTENSION_WAV).let { - contentResolver.openOutputStream(it.uri) ?: return - } + val outputStream = (application as App).container.fileRepository + .getOutputFile(FILE_NAME_EXTENSION_WAV) + .let { + contentResolver.openOutputStream(it.uri) ?: return + } pcmConverter?.convertToWave(inputStream, outputStream, BUFFER_SIZE_IN_BYTES) outputFile?.delete() } diff --git a/app/src/main/java/com/bnyro/recorder/services/ScreenRecorderService.kt b/app/src/main/java/com/bnyro/recorder/services/ScreenRecorderService.kt index ce57e48e..20ab3cb2 100644 --- a/app/src/main/java/com/bnyro/recorder/services/ScreenRecorderService.kt +++ b/app/src/main/java/com/bnyro/recorder/services/ScreenRecorderService.kt @@ -13,6 +13,7 @@ import android.util.DisplayMetrics import android.util.Log import android.view.Display import androidx.activity.result.ActivityResult +import com.bnyro.recorder.App import com.bnyro.recorder.R import com.bnyro.recorder.enums.AudioChannels import com.bnyro.recorder.enums.AudioDeviceSource @@ -21,7 +22,6 @@ import com.bnyro.recorder.enums.VideoFormat import com.bnyro.recorder.obj.VideoResolution import com.bnyro.recorder.util.PlayerHelper import com.bnyro.recorder.util.Preferences -import com.bnyro.recorder.util.StorageHelper class ScreenRecorderService : RecorderService() { override val notificationTitle: String @@ -115,10 +115,8 @@ class ScreenRecorderService : RecorderService() { null ) - outputFile = StorageHelper.getOutputFile( - this@ScreenRecorderService, - videoFormat.extension - ) + outputFile = (application as App).container.fileRepository + .getOutputFile(videoFormat.extension) fileDescriptor = contentResolver.openFileDescriptor(outputFile!!.uri, "w") setOutputFile(fileDescriptor?.fileDescriptor) diff --git a/app/src/main/java/com/bnyro/recorder/ui/components/NamingPatternPref.kt b/app/src/main/java/com/bnyro/recorder/ui/components/NamingPatternPref.kt index 20fb9ea4..c69db8c8 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/components/NamingPatternPref.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/components/NamingPatternPref.kt @@ -14,8 +14,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.bnyro.recorder.R import com.bnyro.recorder.ui.common.DialogButton +import com.bnyro.recorder.util.FileRepositoryImpl import com.bnyro.recorder.util.Preferences -import com.bnyro.recorder.util.StorageHelper @Composable fun NamingPatternPref() { @@ -48,7 +48,7 @@ fun NamingPatternPref() { mutableStateOf( Preferences.getString( Preferences.namingPatternKey, - StorageHelper.DEFAULT_NAMING_PATTERN + FileRepositoryImpl.DEFAULT_NAMING_PATTERN ) ) } diff --git a/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt b/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt index edff9ee9..5b6d7a1d 100644 --- a/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt +++ b/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt @@ -1,28 +1,34 @@ package com.bnyro.recorder.util +import android.annotation.SuppressLint import android.content.Context import android.media.MediaMetadataRetriever +import android.net.Uri import androidx.documentfile.provider.DocumentFile import com.bnyro.recorder.enums.RecorderType import com.bnyro.recorder.enums.SortOrder import com.bnyro.recorder.obj.RecordingItemData +import java.text.SimpleDateFormat +import java.util.Calendar interface FileRepository { suspend fun getVideoRecordingItems(sortOrder: SortOrder): List suspend fun getAudioRecordingItems(sortOrder: SortOrder): List suspend fun deleteSelectedFiles(files: List) suspend fun deleteAllFiles() + fun getOutputFile(extension: String): DocumentFile + fun getOutputDir(): DocumentFile } class FileRepositoryImpl(val context: Context) : FileRepository { private fun getVideoFiles(): List = - StorageHelper.getOutputDir(context).listFiles().filter { + getOutputDir().listFiles().filter { it.isFile && it.name.orEmpty().endsWith("mp4") } private fun getAudioFiles(): List = - StorageHelper.getOutputDir(context).listFiles().filter { + getOutputDir().listFiles().filter { it.isFile && !it.name.orEmpty().endsWith("mp4") } @@ -50,10 +56,48 @@ class FileRepositoryImpl(val context: Context) : FileRepository { } override suspend fun deleteAllFiles() { - StorageHelper.getOutputDir(context).listFiles().forEach { + getOutputDir().listFiles().forEach { if (it.isFile) it.delete() } } + + override fun getOutputFile(extension: String): DocumentFile { + val currentTimeMillis = Calendar.getInstance().time + val currentDateTime = dateTimeFormat.format(currentTimeMillis) + val currentDate = currentDateTime.split("_").first() + val currentTime = currentDateTime.split("_").last() + + val fileName = Preferences.getString( + Preferences.namingPatternKey, + DEFAULT_NAMING_PATTERN + ) + .replace("%d", currentDate) + .replace("%t", currentTime) + .replace("%m", currentTimeMillis.time.toString()) + .replace("%s", currentTimeMillis.time.div(1000).toString()) + + val recordingFile = getOutputDir().createFile("audio/*", "$fileName.$extension") + return recordingFile!! + } + + override fun getOutputDir(): DocumentFile { + val prefDir = Preferences.prefs.getString(Preferences.targetFolderKey, "") + val audioDir = when { + prefDir.isNullOrBlank() -> { + val dir = context.getExternalFilesDir(null) ?: context.filesDir + DocumentFile.fromFile(dir) + } + + else -> DocumentFile.fromTreeUri(context, Uri.parse(prefDir)) + } + return audioDir!! + } + + companion object { + @SuppressLint("SimpleDateFormat") + private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss") + const val DEFAULT_NAMING_PATTERN = "%d_%t" + } } fun List.sortedBy(sortOrder: SortOrder): List { diff --git a/app/src/main/java/com/bnyro/recorder/util/StorageHelper.kt b/app/src/main/java/com/bnyro/recorder/util/StorageHelper.kt deleted file mode 100644 index 077ae225..00000000 --- a/app/src/main/java/com/bnyro/recorder/util/StorageHelper.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.bnyro.recorder.util - -import android.annotation.SuppressLint -import android.content.Context -import android.net.Uri -import androidx.documentfile.provider.DocumentFile -import java.text.SimpleDateFormat -import java.util.Calendar - -object StorageHelper { - @SuppressLint("SimpleDateFormat") - private val dateTimeFormat = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss") - const val DEFAULT_NAMING_PATTERN = "%d_%t" - - fun getOutputFile(context: Context, extension: String): DocumentFile { - val currentTimeMillis = Calendar.getInstance().time - val currentDateTime = dateTimeFormat.format(currentTimeMillis) - val currentDate = currentDateTime.split("_").first() - val currentTime = currentDateTime.split("_").last() - - val fileName = Preferences.getString(Preferences.namingPatternKey, DEFAULT_NAMING_PATTERN) - .replace("%d", currentDate) - .replace("%t", currentTime) - .replace("%m", currentTimeMillis.time.toString()) - .replace("%s", currentTimeMillis.time.div(1000).toString()) - - val recordingFile = getOutputDir(context).createFile("audio/*", "$fileName.$extension") - return recordingFile!! - } - - fun getOutputDir(context: Context): DocumentFile { - val prefDir = Preferences.prefs.getString(Preferences.targetFolderKey, "") - val audioDir = when { - prefDir.isNullOrBlank() -> { - val dir = context.getExternalFilesDir(null) ?: context.filesDir - DocumentFile.fromFile(dir) - } - else -> DocumentFile.fromTreeUri(context, Uri.parse(prefDir)) - } - return audioDir!! - } -} From 8cea7d68683608769c65d29aaaacb10df98ac0f4 Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Sat, 29 Jul 2023 16:20:09 +0530 Subject: [PATCH 3/5] Use Background threads for file loading --- .../bnyro/recorder/ui/models/PlayerModel.kt | 3 +- .../com/bnyro/recorder/util/FileRepository.kt | 28 ++++++++++++------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt b/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt index 13479085..966b79b6 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt @@ -8,7 +8,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.documentfile.provider.DocumentFile import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewmodel.initializer @@ -107,7 +106,7 @@ class PlayerModel(private val fileRepository: FileRepository) : ViewModel() { } companion object { - val Factory: ViewModelProvider.Factory = viewModelFactory { + val Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as App) PlayerModel(application.container.fileRepository) diff --git a/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt b/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt index 5b6d7a1d..e06624cb 100644 --- a/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt +++ b/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt @@ -10,6 +10,8 @@ import com.bnyro.recorder.enums.SortOrder import com.bnyro.recorder.obj.RecordingItemData import java.text.SimpleDateFormat import java.util.Calendar +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext interface FileRepository { suspend fun getVideoRecordingItems(sortOrder: SortOrder): List @@ -33,20 +35,26 @@ class FileRepositoryImpl(val context: Context) : FileRepository { } override suspend fun getVideoRecordingItems(sortOrder: SortOrder): List { - return getVideoFiles().sortedBy(sortOrder).map { - val thumbnail = - MediaMetadataRetriever().apply { - setDataSource( - context, - it.uri - ) - }.frameAtTime - RecordingItemData(it, RecorderType.VIDEO, thumbnail) + val items = withContext(Dispatchers.IO) { + getVideoFiles().sortedBy(sortOrder).map { + val thumbnail = + MediaMetadataRetriever().apply { + setDataSource( + context, + it.uri + ) + }.frameAtTime + RecordingItemData(it, RecorderType.VIDEO, thumbnail) + } } + return items } override suspend fun getAudioRecordingItems(sortOrder: SortOrder): List { - return getAudioFiles().sortedBy(sortOrder).map { RecordingItemData(it, RecorderType.AUDIO) } + val items = withContext(Dispatchers.IO) { + getAudioFiles().sortedBy(sortOrder).map { RecordingItemData(it, RecorderType.AUDIO) } + } + return items } override suspend fun deleteSelectedFiles(files: List) { From cfdd73d594a253447e02ecadf2dfcf25a779e72b Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Sat, 29 Jul 2023 16:22:32 +0530 Subject: [PATCH 4/5] Use Background threads for file deleting --- .../java/com/bnyro/recorder/util/FileRepository.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt b/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt index e06624cb..40ea77fd 100644 --- a/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt +++ b/app/src/main/java/com/bnyro/recorder/util/FileRepository.kt @@ -58,14 +58,18 @@ class FileRepositoryImpl(val context: Context) : FileRepository { } override suspend fun deleteSelectedFiles(files: List) { - files.forEach { - if (it.exists()) it.delete() + withContext(Dispatchers.IO) { + files.forEach { + if (it.exists()) it.delete() + } } } override suspend fun deleteAllFiles() { - getOutputDir().listFiles().forEach { - if (it.isFile) it.delete() + withContext(Dispatchers.IO) { + getOutputDir().listFiles().forEach { + if (it.isFile) it.delete() + } } } From 7cf7b2fb2022d48b4581cd8422693fc9f8aafd8e Mon Sep 17 00:00:00 2001 From: Suhas Dissanayake Date: Sat, 29 Jul 2023 16:28:37 +0530 Subject: [PATCH 5/5] Remove app container --- app/src/main/java/com/bnyro/recorder/App.kt | 7 +++++-- app/src/main/java/com/bnyro/recorder/AppContainer.kt | 11 ----------- .../receivers/FinishedNotificationReceiver.kt | 2 +- .../bnyro/recorder/services/AudioRecorderService.kt | 2 +- .../recorder/services/LosslessRecorderService.kt | 2 +- .../bnyro/recorder/services/ScreenRecorderService.kt | 2 +- .../java/com/bnyro/recorder/ui/models/PlayerModel.kt | 2 +- 7 files changed, 10 insertions(+), 18 deletions(-) delete mode 100644 app/src/main/java/com/bnyro/recorder/AppContainer.kt diff --git a/app/src/main/java/com/bnyro/recorder/App.kt b/app/src/main/java/com/bnyro/recorder/App.kt index d3524dd7..e2025d19 100644 --- a/app/src/main/java/com/bnyro/recorder/App.kt +++ b/app/src/main/java/com/bnyro/recorder/App.kt @@ -1,17 +1,20 @@ package com.bnyro.recorder import android.app.Application +import com.bnyro.recorder.util.FileRepository +import com.bnyro.recorder.util.FileRepositoryImpl import com.bnyro.recorder.util.NotificationHelper import com.bnyro.recorder.util.Preferences import com.bnyro.recorder.util.ShortcutHelper class App : Application() { - lateinit var container: AppContainer + val fileRepository: FileRepository by lazy { + FileRepositoryImpl(this) + } override fun onCreate() { super.onCreate() Preferences.init(this) NotificationHelper.buildNotificationChannels(this) ShortcutHelper.createShortcuts(this) - container = AppContainer(this) } } diff --git a/app/src/main/java/com/bnyro/recorder/AppContainer.kt b/app/src/main/java/com/bnyro/recorder/AppContainer.kt deleted file mode 100644 index e3575d64..00000000 --- a/app/src/main/java/com/bnyro/recorder/AppContainer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.bnyro.recorder - -import android.content.Context -import com.bnyro.recorder.util.FileRepository -import com.bnyro.recorder.util.FileRepositoryImpl - -class AppContainer(context: Context) { - val fileRepository: FileRepository by lazy { - FileRepositoryImpl(context) - } -} diff --git a/app/src/main/java/com/bnyro/recorder/receivers/FinishedNotificationReceiver.kt b/app/src/main/java/com/bnyro/recorder/receivers/FinishedNotificationReceiver.kt index 65e029ce..08a890d3 100644 --- a/app/src/main/java/com/bnyro/recorder/receivers/FinishedNotificationReceiver.kt +++ b/app/src/main/java/com/bnyro/recorder/receivers/FinishedNotificationReceiver.kt @@ -12,7 +12,7 @@ import com.bnyro.recorder.util.NotificationHelper class FinishedNotificationReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val fileName = intent.getStringExtra(RecorderService.FILE_NAME_EXTRA_KEY) ?: return - val file = (context.applicationContext as App).container.fileRepository + val file = (context.applicationContext as App).fileRepository .getOutputDir().findFile(fileName) when (intent.getStringExtra(RecorderService.ACTION_EXTRA_KEY)) { diff --git a/app/src/main/java/com/bnyro/recorder/services/AudioRecorderService.kt b/app/src/main/java/com/bnyro/recorder/services/AudioRecorderService.kt index 0591e62f..25c52b8b 100644 --- a/app/src/main/java/com/bnyro/recorder/services/AudioRecorderService.kt +++ b/app/src/main/java/com/bnyro/recorder/services/AudioRecorderService.kt @@ -38,7 +38,7 @@ class AudioRecorderService : RecorderService() { setOutputFormat(audioFormat.format) setAudioEncoder(audioFormat.codec) - outputFile = (application as App).container.fileRepository.getOutputFile( + outputFile = (application as App).fileRepository.getOutputFile( audioFormat.extension ) fileDescriptor = contentResolver.openFileDescriptor(outputFile!!.uri, "w") diff --git a/app/src/main/java/com/bnyro/recorder/services/LosslessRecorderService.kt b/app/src/main/java/com/bnyro/recorder/services/LosslessRecorderService.kt index edaabb6d..5e779763 100644 --- a/app/src/main/java/com/bnyro/recorder/services/LosslessRecorderService.kt +++ b/app/src/main/java/com/bnyro/recorder/services/LosslessRecorderService.kt @@ -110,7 +110,7 @@ class LosslessRecorderService : RecorderService() { private fun convertToWav() { val inputStream = contentResolver.openInputStream(outputFile?.uri ?: return) ?: return - val outputStream = (application as App).container.fileRepository + val outputStream = (application as App).fileRepository .getOutputFile(FILE_NAME_EXTENSION_WAV) .let { contentResolver.openOutputStream(it.uri) ?: return diff --git a/app/src/main/java/com/bnyro/recorder/services/ScreenRecorderService.kt b/app/src/main/java/com/bnyro/recorder/services/ScreenRecorderService.kt index 20ab3cb2..c2e6e1d6 100644 --- a/app/src/main/java/com/bnyro/recorder/services/ScreenRecorderService.kt +++ b/app/src/main/java/com/bnyro/recorder/services/ScreenRecorderService.kt @@ -115,7 +115,7 @@ class ScreenRecorderService : RecorderService() { null ) - outputFile = (application as App).container.fileRepository + outputFile = (application as App).fileRepository .getOutputFile(videoFormat.extension) fileDescriptor = contentResolver.openFileDescriptor(outputFile!!.uri, "w") setOutputFile(fileDescriptor?.fileDescriptor) diff --git a/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt b/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt index 966b79b6..531ae214 100644 --- a/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt +++ b/app/src/main/java/com/bnyro/recorder/ui/models/PlayerModel.kt @@ -109,7 +109,7 @@ class PlayerModel(private val fileRepository: FileRepository) : ViewModel() { val Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as App) - PlayerModel(application.container.fileRepository) + PlayerModel(application.fileRepository) } } }