Skip to content

Commit 4438209

Browse files
committedJan 8, 2025
Simplify storage access
Closes #40 As a side effect, the following issues were also addressed: Closes #24 Closes #60 Closes #74
·
1.3.31.1.0
1 parent 060ba08 commit 4438209

File tree

15 files changed

+363
-486
lines changed

15 files changed

+363
-486
lines changed
 

‎app/src/main/kotlin/org/fossify/voicerecorder/activities/MainActivity.kt

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ import org.fossify.voicerecorder.BuildConfig
3030
import org.fossify.voicerecorder.R
3131
import org.fossify.voicerecorder.adapters.ViewPagerAdapter
3232
import org.fossify.voicerecorder.databinding.ActivityMainBinding
33-
import org.fossify.voicerecorder.extensions.checkRecycleBinItems
3433
import org.fossify.voicerecorder.extensions.config
34+
import org.fossify.voicerecorder.extensions.deleteExpiredTrashedRecordings
35+
import org.fossify.voicerecorder.extensions.ensureStoragePermission
3536
import org.fossify.voicerecorder.helpers.STOP_AMPLITUDE_UPDATE
3637
import org.fossify.voicerecorder.models.Events
3738
import org.fossify.voicerecorder.services.RecorderService
@@ -65,7 +66,7 @@ class MainActivity : SimpleActivity() {
6566
}
6667

6768
if (savedInstanceState == null) {
68-
checkRecycleBinItems()
69+
deleteExpiredTrashedRecordings()
6970
}
7071

7172
handlePermission(PERMISSION_RECORD_AUDIO) {
@@ -172,12 +173,20 @@ class MainActivity : SimpleActivity() {
172173

173174
private fun tryInitVoiceRecorder() {
174175
if (isRPlus()) {
175-
setupViewPager()
176+
ensureStoragePermission { granted ->
177+
if (granted) {
178+
setupViewPager()
179+
} else {
180+
toast(org.fossify.commons.R.string.no_storage_permissions)
181+
finish()
182+
}
183+
}
176184
} else {
177185
handlePermission(PERMISSION_WRITE_STORAGE) {
178186
if (it) {
179187
setupViewPager()
180188
} else {
189+
toast(org.fossify.commons.R.string.no_storage_permissions)
181190
finish()
182191
}
183192
}

‎app/src/main/kotlin/org/fossify/voicerecorder/activities/SettingsActivity.kt

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import android.media.MediaRecorder
55
import android.os.Bundle
66
import org.fossify.commons.dialogs.ChangeDateTimeFormatDialog
77
import org.fossify.commons.dialogs.ConfirmationDialog
8-
import org.fossify.commons.dialogs.FeatureLockedDialog
9-
import org.fossify.commons.dialogs.FilePickerDialog
108
import org.fossify.commons.dialogs.RadioGroupDialog
119
import org.fossify.commons.extensions.addLockedLabelIfNeeded
1210
import org.fossify.commons.extensions.beGone
@@ -31,8 +29,9 @@ import org.fossify.commons.models.RadioItem
3129
import org.fossify.voicerecorder.R
3230
import org.fossify.voicerecorder.databinding.ActivitySettingsBinding
3331
import org.fossify.voicerecorder.extensions.config
34-
import org.fossify.voicerecorder.extensions.emptyTheRecycleBin
32+
import org.fossify.voicerecorder.extensions.deleteTrashedRecordings
3533
import org.fossify.voicerecorder.extensions.getAllRecordings
34+
import org.fossify.voicerecorder.extensions.launchFilePickerDialog
3635
import org.fossify.voicerecorder.helpers.BITRATES
3736
import org.fossify.voicerecorder.helpers.EXTENSION_M4A
3837
import org.fossify.voicerecorder.helpers.EXTENSION_MP3
@@ -158,27 +157,8 @@ class SettingsActivity : SimpleActivity() {
158157
addLockedLabelIfNeeded(R.string.save_recordings_in)
159158
binding.settingsSaveRecordings.text = humanizePath(config.saveRecordingsFolder)
160159
binding.settingsSaveRecordingsHolder.setOnClickListener {
161-
if (isOrWasThankYouInstalled()) {
162-
FilePickerDialog(this, config.saveRecordingsFolder, false, showFAB = true) {
163-
val path = it
164-
handleSAFDialog(path) { grantedSAF ->
165-
if (!grantedSAF) {
166-
return@handleSAFDialog
167-
}
168-
169-
handleSAFDialogSdk30(path) { grantedSAF30 ->
170-
if (!grantedSAF30) {
171-
return@handleSAFDialogSdk30
172-
}
173-
174-
config.saveRecordingsFolder = path
175-
binding.settingsSaveRecordings.text =
176-
humanizePath(config.saveRecordingsFolder)
177-
}
178-
}
179-
}
180-
} else {
181-
FeatureLockedDialog(this) { }
160+
launchFilePickerDialog {
161+
binding.settingsSaveRecordings.text = humanizePath(config.saveRecordingsFolder)
182162
}
183163
}
184164
}
@@ -272,7 +252,7 @@ class SettingsActivity : SimpleActivity() {
272252
negative = org.fossify.commons.R.string.no
273253
) {
274254
ensureBackgroundThread {
275-
emptyTheRecycleBin()
255+
deleteTrashedRecordings()
276256
runOnUiThread {
277257
recycleBinContentSize = 0
278258
binding.settingsEmptyRecycleBinSize.text = 0.formatSize()

‎app/src/main/kotlin/org/fossify/voicerecorder/adapters/RecordingsAdapter.kt

Lines changed: 8 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ import org.fossify.commons.extensions.openPathIntent
1919
import org.fossify.commons.extensions.setupViewBackground
2020
import org.fossify.commons.extensions.sharePathsIntent
2121
import org.fossify.commons.helpers.ensureBackgroundThread
22-
import org.fossify.commons.helpers.isQPlus
2322
import org.fossify.commons.views.MyRecyclerView
2423
import org.fossify.voicerecorder.BuildConfig
2524
import org.fossify.voicerecorder.R
@@ -29,8 +28,7 @@ import org.fossify.voicerecorder.dialogs.DeleteConfirmationDialog
2928
import org.fossify.voicerecorder.dialogs.RenameRecordingDialog
3029
import org.fossify.voicerecorder.extensions.config
3130
import org.fossify.voicerecorder.extensions.deleteRecordings
32-
import org.fossify.voicerecorder.extensions.moveRecordingsToRecycleBin
33-
import org.fossify.voicerecorder.helpers.getAudioFileContentUri
31+
import org.fossify.voicerecorder.extensions.trashRecordings
3432
import org.fossify.voicerecorder.interfaces.RefreshRecordingsListener
3533
import org.fossify.voicerecorder.models.Events
3634
import org.fossify.voicerecorder.models.Recording
@@ -133,23 +131,13 @@ class RecordingsAdapter(
133131

134132
private fun openRecordingWith() {
135133
val recording = getItemWithKey(selectedKeys.first()) ?: return
136-
val path = if (isQPlus()) {
137-
getAudioFileContentUri(recording.id.toLong()).toString()
138-
} else {
139-
recording.path
140-
}
141-
134+
val path = recording.path
142135
activity.openPathIntent(path, false, BuildConfig.APPLICATION_ID, "audio/*")
143136
}
144137

145138
private fun shareRecordings() {
146139
val selectedItems = getSelectedItems()
147-
val paths = selectedItems.map {
148-
it.path.ifEmpty {
149-
getAudioFileContentUri(it.id.toLong()).toString()
150-
}
151-
}
152-
140+
val paths = selectedItems.map { it.path }
153141
activity.sharePathsIntent(paths, BuildConfig.APPLICATION_ID)
154142
}
155143

@@ -177,15 +165,15 @@ class RecordingsAdapter(
177165
ensureBackgroundThread {
178166
val toRecycleBin = !skipRecycleBin && activity.config.useRecycleBin
179167
if (toRecycleBin) {
180-
moveMediaStoreRecordingsToRecycleBin()
168+
trashRecordings()
181169
} else {
182-
deleteMediaStoreRecordings()
170+
deleteRecordings()
183171
}
184172
}
185173
}
186174
}
187175

188-
private fun deleteMediaStoreRecordings() {
176+
private fun deleteRecordings() {
189177
if (selectedKeys.isEmpty()) {
190178
return
191179
}
@@ -203,7 +191,7 @@ class RecordingsAdapter(
203191
}
204192
}
205193

206-
private fun moveMediaStoreRecordingsToRecycleBin() {
194+
private fun trashRecordings() {
207195
if (selectedKeys.isEmpty()) {
208196
return
209197
}
@@ -214,7 +202,7 @@ class RecordingsAdapter(
214202

215203
val positions = getSelectedItemPositions()
216204

217-
activity.moveRecordingsToRecycleBin(recordingsToRemove) { success ->
205+
activity.trashRecordings(recordingsToRemove) { success ->
218206
if (success) {
219207
doDeleteAnimation(oldRecordingIndex, recordingsToRemove, positions)
220208
EventBus.getDefault().post(Events.RecordingTrashUpdated())

‎app/src/main/kotlin/org/fossify/voicerecorder/dialogs/RenameRecordingDialog.kt

Lines changed: 13 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
package org.fossify.voicerecorder.dialogs
22

3-
import android.content.ContentValues
4-
import android.provider.MediaStore.Audio.Media
53
import androidx.appcompat.app.AlertDialog
64
import org.fossify.commons.activities.BaseSimpleActivity
75
import org.fossify.commons.extensions.getAlertDialogBuilder
@@ -19,7 +17,6 @@ import org.fossify.commons.helpers.ensureBackgroundThread
1917
import org.fossify.commons.helpers.isRPlus
2018
import org.fossify.voicerecorder.databinding.DialogRenameRecordingBinding
2119
import org.fossify.voicerecorder.extensions.config
22-
import org.fossify.voicerecorder.helpers.getAudioFileContentUri
2320
import org.fossify.voicerecorder.models.Events
2421
import org.fossify.voicerecorder.models.Recording
2522
import org.greenrobot.eventbus.EventBus
@@ -60,9 +57,9 @@ class RenameRecordingDialog(
6057

6158
ensureBackgroundThread {
6259
if (isRPlus()) {
63-
updateMediaStoreTitle(recording, newTitle)
60+
renameRecording(recording, newTitle)
6461
} else {
65-
updateLegacyFilename(recording, newTitle)
62+
renameRecordingLegacy(recording, newTitle)
6663
}
6764

6865
activity.runOnUiThread {
@@ -75,40 +72,26 @@ class RenameRecordingDialog(
7572
}
7673
}
7774

78-
private fun updateMediaStoreTitle(recording: Recording, newTitle: String) {
75+
private fun renameRecording(recording: Recording, newTitle: String) {
76+
// TODO: IllegalStateException: File already exists
7977
val oldExtension = recording.title.getFilenameExtension()
8078
val newDisplayName = "${newTitle.removeSuffix(".$oldExtension")}.$oldExtension"
8179

82-
val values = ContentValues().apply {
83-
put(Media.TITLE, newTitle.substringAfterLast('.'))
84-
put(Media.DISPLAY_NAME, newDisplayName)
85-
}
86-
87-
// if the old way of renaming fails, try the new SDK 30 one on Android 11+
8880
try {
89-
activity.contentResolver.update(
90-
getAudioFileContentUri(recording.id.toLong()),
91-
values,
92-
null,
93-
null
94-
)
95-
} catch (e: Exception) {
96-
try {
97-
val path = "${activity.config.saveRecordingsFolder}/${recording.title}"
98-
val newPath = "${path.getParentPath()}/$newDisplayName"
99-
activity.handleSAFDialogSdk30(path) {
100-
val success = activity.renameDocumentSdk30(path, newPath)
101-
if (success) {
102-
EventBus.getDefault().post(Events.RecordingCompleted())
103-
}
81+
val path = "${activity.config.saveRecordingsFolder}/${recording.title}"
82+
val newPath = "${path.getParentPath()}/$newDisplayName"
83+
activity.handleSAFDialogSdk30(path) {
84+
val success = activity.renameDocumentSdk30(path, newPath)
85+
if (success) {
86+
EventBus.getDefault().post(Events.RecordingCompleted())
10487
}
105-
} catch (e: Exception) {
106-
activity.showErrorToast(e)
10788
}
89+
} catch (e: Exception) {
90+
activity.showErrorToast(e)
10891
}
10992
}
11093

111-
private fun updateLegacyFilename(recording: Recording, newTitle: String) {
94+
private fun renameRecordingLegacy(recording: Recording, newTitle: String) {
11295
val oldExtension = recording.title.getFilenameExtension()
11396
val oldPath = recording.path
11497
val newFilename = "${newTitle.removeSuffix(".$oldExtension")}.$oldExtension"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package org.fossify.voicerecorder.dialogs
2+
3+
import androidx.appcompat.app.AlertDialog
4+
import org.fossify.commons.activities.BaseSimpleActivity
5+
import org.fossify.commons.databinding.DialogMessageBinding
6+
import org.fossify.commons.extensions.getAlertDialogBuilder
7+
import org.fossify.commons.extensions.setupDialogStuff
8+
import org.fossify.voicerecorder.R
9+
10+
class StoragePermissionDialog(
11+
private val activity: BaseSimpleActivity,
12+
private val callback: (result: Boolean) -> Unit
13+
) {
14+
private var dialog: AlertDialog? = null
15+
16+
init {
17+
val view = DialogMessageBinding.inflate(activity.layoutInflater, null, false)
18+
view.message.text = activity.getString(R.string.confirm_recording_folder)
19+
20+
activity.getAlertDialogBuilder()
21+
.setPositiveButton(org.fossify.commons.R.string.ok) { _, _ ->
22+
callback(true)
23+
}
24+
.apply {
25+
activity.setupDialogStuff(
26+
view = view.root,
27+
dialog = this,
28+
cancelOnTouchOutside = false,
29+
) { alertDialog ->
30+
dialog = alertDialog
31+
}
32+
}
33+
}
34+
}
Lines changed: 171 additions & 139 deletions
Original file line numberDiff line numberDiff line change
@@ -1,194 +1,200 @@
11
package org.fossify.voicerecorder.extensions
22

3-
import android.content.ContentValues
4-
import android.provider.MediaStore
5-
import android.provider.MediaStore.Audio.Media
3+
import android.provider.DocumentsContract
4+
import androidx.core.net.toUri
65
import org.fossify.commons.activities.BaseSimpleActivity
6+
import org.fossify.commons.dialogs.FilePickerDialog
7+
import org.fossify.commons.extensions.createDocumentUriUsingFirstParentTreeUri
78
import org.fossify.commons.extensions.deleteFile
9+
import org.fossify.commons.extensions.getDoesFilePathExist
10+
import org.fossify.commons.extensions.getFilenameExtension
811
import org.fossify.commons.extensions.getParentPath
12+
import org.fossify.commons.extensions.hasProperStoredFirstParentUri
13+
import org.fossify.commons.extensions.renameDocumentSdk30
14+
import org.fossify.commons.extensions.renameFile
15+
import org.fossify.commons.extensions.showErrorToast
916
import org.fossify.commons.extensions.toFileDirItem
1017
import org.fossify.commons.helpers.DAY_SECONDS
1118
import org.fossify.commons.helpers.MONTH_SECONDS
1219
import org.fossify.commons.helpers.ensureBackgroundThread
13-
import org.fossify.commons.helpers.isQPlus
1420
import org.fossify.commons.helpers.isRPlus
1521
import org.fossify.commons.models.FileDirItem
16-
import org.fossify.voicerecorder.helpers.getAudioFileContentUri
22+
import org.fossify.voicerecorder.dialogs.StoragePermissionDialog
23+
import org.fossify.voicerecorder.models.Events
1724
import org.fossify.voicerecorder.models.Recording
25+
import org.greenrobot.eventbus.EventBus
1826
import java.io.File
1927

20-
fun BaseSimpleActivity.deleteRecordings(
21-
recordingsToRemove: Collection<Recording>,
22-
callback: (success: Boolean) -> Unit
23-
) {
24-
when {
25-
isRPlus() -> {
26-
val fileUris = recordingsToRemove.map { recording ->
27-
getAudioFileContentUri(recording.id.toLong())
28-
}
29-
30-
deleteSDK30Uris(fileUris, callback)
28+
fun BaseSimpleActivity.ensureStoragePermission(callback: (result: Boolean) -> Unit) {
29+
if (isRPlus() && !hasProperStoredFirstParentUri(config.saveRecordingsFolder)) {
30+
StoragePermissionDialog(this) {
31+
launchFilePickerDialog(callback)
3132
}
33+
} else {
34+
callback(true)
35+
}
36+
}
3237

33-
isQPlus() -> {
34-
recordingsToRemove.forEach {
35-
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
36-
val selection = "${Media._ID} = ?"
37-
val selectionArgs = arrayOf(it.id.toString())
38-
val result = contentResolver.delete(uri, selection, selectionArgs)
39-
40-
if (result == 0) {
41-
val fileDirItem = File(it.path).toFileDirItem(this)
42-
deleteFile(fileDirItem)
38+
fun BaseSimpleActivity.launchFilePickerDialog(callback: (success: Boolean) -> Unit) {
39+
FilePickerDialog(
40+
activity = this,
41+
currPath = config.saveRecordingsFolder,
42+
pickFile = false,
43+
showFAB = true
44+
) { path ->
45+
handleSAFDialog(path) { grantedSAF ->
46+
if (!grantedSAF) {
47+
callback(false)
48+
return@handleSAFDialog
49+
}
50+
51+
handleSAFDialogSdk30(path) { grantedSAF30 ->
52+
if (!grantedSAF30) {
53+
callback(false)
54+
return@handleSAFDialogSdk30
4355
}
56+
57+
config.saveRecordingsFolder = path
58+
callback(true)
4459
}
45-
callback(true)
4660
}
61+
}
62+
}
4763

48-
else -> {
64+
fun BaseSimpleActivity.deleteRecordings(
65+
recordingsToRemove: Collection<Recording>,
66+
callback: (success: Boolean) -> Unit
67+
) {
68+
ensureBackgroundThread {
69+
if (isRPlus()) {
70+
val resolver = contentResolver
71+
recordingsToRemove.forEach {
72+
DocumentsContract.deleteDocument(resolver, it.path.toUri())
73+
}
74+
} else {
4975
recordingsToRemove.forEach {
5076
val fileDirItem = File(it.path).toFileDirItem(this)
5177
deleteFile(fileDirItem)
5278
}
53-
callback(true)
5479
}
80+
81+
callback(true)
5582
}
5683
}
5784

85+
fun BaseSimpleActivity.trashRecordings(
86+
recordingsToMove: Collection<Recording>,
87+
callback: (success: Boolean) -> Unit
88+
) = moveRecordings(
89+
recordingsToMove = recordingsToMove,
90+
sourceParent = config.saveRecordingsFolder,
91+
destinationParent = getOrCreateTrashFolder(),
92+
callback = callback
93+
)
94+
5895
fun BaseSimpleActivity.restoreRecordings(
5996
recordingsToRestore: Collection<Recording>,
6097
callback: (success: Boolean) -> Unit
98+
) = moveRecordings(
99+
recordingsToMove = recordingsToRestore,
100+
sourceParent = getOrCreateTrashFolder(),
101+
destinationParent = config.saveRecordingsFolder,
102+
callback = callback
103+
)
104+
105+
fun BaseSimpleActivity.moveRecordings(
106+
recordingsToMove: Collection<Recording>,
107+
sourceParent: String,
108+
destinationParent: String,
109+
callback: (success: Boolean) -> Unit
61110
) {
62-
when {
63-
isRPlus() -> {
64-
val fileUris = recordingsToRestore.map { recording ->
65-
getAudioFileContentUri(recording.id.toLong())
66-
}
67-
68-
trashSDK30Uris(fileUris, false, callback)
69-
}
70-
71-
isQPlus() -> {
72-
var wait = false
73-
recordingsToRestore.forEach {
74-
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
75-
val selection = "${Media._ID} = ?"
76-
val selectionArgs = arrayOf(it.id.toString())
77-
val values = ContentValues().apply {
78-
put(Media.IS_TRASHED, 0)
79-
}
80-
val result = contentResolver.update(uri, values, selection, selectionArgs)
81-
82-
if (result == 0) {
83-
wait = true
84-
copyMoveFilesTo(
85-
fileDirItems = arrayListOf(File(it.path).toFileDirItem(this)),
86-
source = it.path.getParentPath(),
87-
destination = config.saveRecordingsFolder,
88-
isCopyOperation = false,
89-
copyPhotoVideoOnly = false,
90-
copyHidden = false
91-
) {
92-
callback(true)
93-
}
94-
}
95-
}
96-
if (!wait) {
97-
callback(true)
98-
}
99-
}
100-
101-
else -> {
102-
copyMoveFilesTo(
103-
fileDirItems = recordingsToRestore
104-
.map { File(it.path).toFileDirItem(this) }
105-
.toMutableList() as ArrayList<FileDirItem>,
106-
source = recordingsToRestore.first().path.getParentPath(),
107-
destination = config.saveRecordingsFolder,
108-
isCopyOperation = false,
109-
copyPhotoVideoOnly = false,
110-
copyHidden = false
111-
) {
112-
callback(true)
113-
}
114-
}
111+
if (isRPlus()) {
112+
moveRecordingsSAF(
113+
recordings = recordingsToMove,
114+
sourceParent = sourceParent,
115+
destinationParent = destinationParent,
116+
callback = callback
117+
)
118+
} else {
119+
moveRecordingsLegacy(
120+
recordings = recordingsToMove,
121+
sourceParent = sourceParent,
122+
destinationParent = destinationParent,
123+
callback = callback
124+
)
115125
}
116126
}
117127

118-
fun BaseSimpleActivity.moveRecordingsToRecycleBin(
119-
recordingsToMove: Collection<Recording>,
128+
private fun BaseSimpleActivity.moveRecordingsSAF(
129+
recordings: Collection<Recording>,
130+
sourceParent: String,
131+
destinationParent: String,
120132
callback: (success: Boolean) -> Unit
121133
) {
122-
when {
123-
isRPlus() -> {
124-
val fileUris = recordingsToMove.map { recording ->
125-
getAudioFileContentUri(recording.id.toLong())
126-
}
127-
128-
trashSDK30Uris(fileUris, true, callback)
129-
}
130-
131-
isQPlus() -> {
132-
var wait = false
133-
recordingsToMove.forEach {
134-
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
135-
val selection = "${Media._ID} = ?"
136-
val selectionArgs = arrayOf(it.id.toString())
137-
val values = ContentValues().apply {
138-
put(Media.IS_TRASHED, 1)
139-
}
140-
val result = contentResolver.update(uri, values, selection, selectionArgs)
141-
142-
if (result == 0) {
143-
wait = true
144-
copyMoveFilesTo(
145-
fileDirItems = arrayListOf(File(it.path).toFileDirItem(this)),
146-
source = it.path.getParentPath(),
147-
destination = getOrCreateTrashFolder(),
148-
isCopyOperation = false,
149-
copyPhotoVideoOnly = false,
150-
copyHidden = false
151-
) {
152-
callback(true)
134+
ensureBackgroundThread {
135+
val contentResolver = contentResolver
136+
val sourceParentDocumentUri = createDocumentUriUsingFirstParentTreeUri(sourceParent)
137+
val destinationParentDocumentUri =
138+
createDocumentUriUsingFirstParentTreeUri(destinationParent)
139+
recordings.forEach { recording ->
140+
if (getDoesFilePathExist(File(destinationParent, recording.title).absolutePath)) {
141+
renameRecording(recording, recording.title) { success ->
142+
if (success) {
143+
DocumentsContract.moveDocument(
144+
contentResolver,
145+
recording.path.toUri(),
146+
sourceParentDocumentUri,
147+
destinationParentDocumentUri
148+
)
153149
}
154150
}
155-
}
156-
if (!wait) {
157-
callback(true)
151+
} else {
152+
DocumentsContract.moveDocument(
153+
contentResolver,
154+
recording.path.toUri(),
155+
sourceParentDocumentUri,
156+
destinationParentDocumentUri
157+
)
158158
}
159159
}
160160

161-
else -> {
162-
copyMoveFilesTo(
163-
fileDirItems = recordingsToMove
164-
.map { File(it.path).toFileDirItem(this) }
165-
.toMutableList() as ArrayList<FileDirItem>,
166-
source = recordingsToMove.first().path.getParentPath(),
167-
destination = getOrCreateTrashFolder(),
168-
isCopyOperation = false,
169-
copyPhotoVideoOnly = false,
170-
copyHidden = false
171-
) {
172-
callback(true)
173-
}
174-
}
161+
callback(true)
175162
}
176163
}
177164

178-
fun BaseSimpleActivity.checkRecycleBinItems() {
179-
if (isQPlus()) {
180-
// System is handling recycle bin on Q+ devices
181-
return
165+
private fun BaseSimpleActivity.moveRecordingsLegacy(
166+
recordings: Collection<Recording>,
167+
sourceParent: String,
168+
destinationParent: String,
169+
callback: (success: Boolean) -> Unit
170+
) {
171+
copyMoveFilesTo(
172+
fileDirItems = recordings
173+
.map { File(it.path).toFileDirItem(this) }
174+
.toMutableList() as ArrayList<FileDirItem>,
175+
source = sourceParent,
176+
destination = destinationParent,
177+
isCopyOperation = false,
178+
copyPhotoVideoOnly = false,
179+
copyHidden = false
180+
) {
181+
callback(true)
182182
}
183+
}
184+
185+
fun BaseSimpleActivity.deleteTrashedRecordings() {
186+
deleteRecordings(getAllRecordings(trashed = true)) {}
187+
}
183188

189+
fun BaseSimpleActivity.deleteExpiredTrashedRecordings() {
184190
if (
185191
config.useRecycleBin &&
186192
config.lastRecycleBinCheck < System.currentTimeMillis() - DAY_SECONDS * 1000
187193
) {
188194
config.lastRecycleBinCheck = System.currentTimeMillis()
189195
ensureBackgroundThread {
190196
try {
191-
val recordingsToRemove = getLegacyRecordings(trashed = true)
197+
val recordingsToRemove = getAllRecordings(trashed = true)
192198
.filter { it.timestamp < System.currentTimeMillis() - MONTH_SECONDS * 1000L }
193199
if (recordingsToRemove.isNotEmpty()) {
194200
deleteRecordings(recordingsToRemove) {}
@@ -200,6 +206,32 @@ fun BaseSimpleActivity.checkRecycleBinItems() {
200206
}
201207
}
202208

203-
fun BaseSimpleActivity.emptyTheRecycleBin() {
204-
deleteRecordings(getAllRecordings(trashed = true)) {}
209+
private fun BaseSimpleActivity.renameRecording(
210+
recording: Recording,
211+
newTitle: String,
212+
callback: (success: Boolean) -> Unit
213+
) {
214+
val oldExtension = recording.title.getFilenameExtension()
215+
val newDisplayName = "${newTitle.removeSuffix(".$oldExtension")}.$oldExtension"
216+
217+
try {
218+
val path = "${config.saveRecordingsFolder}/${recording.title}"
219+
val newPath = "${path.getParentPath()}/$newDisplayName"
220+
handleSAFDialogSdk30(path) {
221+
val success = renameDocumentSdk30(path, newPath)
222+
if (success) {
223+
EventBus.getDefault().post(Events.RecordingCompleted())
224+
}
225+
}
226+
} catch (e: Exception) {
227+
showErrorToast(e)
228+
}
205229
}
230+
231+
private fun BaseSimpleActivity.renameRecordingLegacy(recording: Recording, newTitle: String) {
232+
val oldExtension = recording.title.getFilenameExtension()
233+
val oldPath = recording.path
234+
val newFilename = "${newTitle.removeSuffix(".$oldExtension")}.$oldExtension"
235+
val newPath = File(oldPath.getParentPath(), newFilename).absolutePath
236+
renameFile(oldPath, newPath, false)
237+
}

‎app/src/main/kotlin/org/fossify/voicerecorder/extensions/Context.kt

Lines changed: 58 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,34 @@
11
package org.fossify.voicerecorder.extensions
22

3-
import android.annotation.SuppressLint
43
import android.appwidget.AppWidgetManager
54
import android.content.ComponentName
6-
import android.content.ContentResolver
75
import android.content.Context
86
import android.content.Intent
9-
import android.database.Cursor
107
import android.graphics.Bitmap
118
import android.graphics.Canvas
129
import android.graphics.drawable.Drawable
1310
import android.media.MediaMetadataRetriever
1411
import android.net.Uri
15-
import android.os.Bundle
16-
import android.os.Environment
17-
import android.provider.MediaStore
18-
import android.provider.MediaStore.Audio.Media
12+
import androidx.documentfile.provider.DocumentFile
1913
import org.fossify.commons.extensions.getDocumentSdk30
2014
import org.fossify.commons.extensions.getDuration
21-
import org.fossify.commons.extensions.getIntValue
22-
import org.fossify.commons.extensions.getLongValue
23-
import org.fossify.commons.extensions.getStringValue
2415
import org.fossify.commons.extensions.internalStoragePath
2516
import org.fossify.commons.extensions.isAudioFast
26-
import org.fossify.commons.extensions.queryCursor
27-
import org.fossify.commons.helpers.isQPlus
2817
import org.fossify.commons.helpers.isRPlus
29-
import org.fossify.voicerecorder.R
3018
import org.fossify.voicerecorder.helpers.Config
19+
import org.fossify.voicerecorder.helpers.DEFAULT_RECORDINGS_FOLDER
3120
import org.fossify.voicerecorder.helpers.IS_RECORDING
3221
import org.fossify.voicerecorder.helpers.MyWidgetRecordDisplayProvider
3322
import org.fossify.voicerecorder.helpers.TOGGLE_WIDGET_UI
34-
import org.fossify.voicerecorder.helpers.getAudioFileContentUri
3523
import org.fossify.voicerecorder.models.Recording
3624
import java.io.File
3725
import kotlin.math.roundToLong
3826

3927
val Context.config: Config get() = Config.newInstance(applicationContext)
4028

29+
val Context.trashFolder
30+
get() = "${config.saveRecordingsFolder}/.trash"
31+
4132
fun Context.drawableToBitmap(drawable: Drawable): Bitmap {
4233
val size = (60 * resources.displayMetrics.density).toInt()
4334
val mutableBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
@@ -65,85 +56,71 @@ fun Context.updateWidgets(isRecording: Boolean) {
6556
}
6657
}
6758

59+
fun Context.getOrCreateTrashFolder(): String {
60+
val folder = File(trashFolder)
61+
if (!folder.exists()) {
62+
folder.mkdir()
63+
}
64+
return trashFolder
65+
}
66+
6867
fun Context.getDefaultRecordingsFolder(): String {
69-
val defaultPath = getDefaultRecordingsRelativePath()
70-
return "$internalStoragePath/$defaultPath"
68+
return "$internalStoragePath/$DEFAULT_RECORDINGS_FOLDER"
7169
}
7270

73-
fun Context.getDefaultRecordingsRelativePath(): String {
74-
return if (isQPlus()) {
75-
"${Environment.DIRECTORY_MUSIC}/Recordings"
71+
fun Context.getAllRecordings(trashed: Boolean = false): ArrayList<Recording> {
72+
return if (isRPlus()) {
73+
val recordings = arrayListOf<Recording>()
74+
recordings.addAll(getRecordings(trashed))
75+
if (trashed) {
76+
// Return recordings trashed using MediaStore, this won't be needed in the future
77+
recordings.addAll(getMediaStoreTrashedRecordings())
78+
}
79+
80+
recordings
7681
} else {
77-
getString(R.string.app_name)
82+
getLegacyRecordings(trashed)
7883
}
7984
}
8085

81-
@SuppressLint("InlinedApi")
82-
fun Context.getNewMediaStoreRecordings(trashed: Boolean = false): ArrayList<Recording> {
86+
private fun Context.getRecordings(trashed: Boolean = false): ArrayList<Recording> {
8387
val recordings = ArrayList<Recording>()
84-
85-
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
86-
val projection = arrayOf(
87-
Media._ID,
88-
Media.DISPLAY_NAME,
89-
Media.DATE_ADDED,
90-
Media.DURATION,
91-
Media.SIZE
92-
)
93-
94-
val bundle = Bundle().apply {
95-
putStringArray(ContentResolver.QUERY_ARG_SORT_COLUMNS, arrayOf(Media.DATE_ADDED))
96-
putInt(
97-
ContentResolver.QUERY_ARG_SORT_DIRECTION,
98-
ContentResolver.QUERY_SORT_DIRECTION_DESCENDING
99-
)
100-
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${Media.OWNER_PACKAGE_NAME} = ?")
101-
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(packageName))
102-
if (config.useRecycleBin) {
103-
val trashedValue = if (trashed) MediaStore.MATCH_ONLY else MediaStore.MATCH_EXCLUDE
104-
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, trashedValue)
88+
val folder = if (trashed) trashFolder else config.saveRecordingsFolder
89+
val files = getDocumentSdk30(folder)?.listFiles() ?: return recordings
90+
files.forEach { file ->
91+
if (file.isAudioRecording()) {
92+
recordings.add(
93+
readRecordingFromFile(file)
94+
)
10595
}
10696
}
107-
queryCursor(uri, projection, bundle, true) { cursor ->
108-
val recording = readRecordingFromCursor(cursor)
109-
recordings.add(recording)
110-
}
11197

11298
return recordings
11399
}
114100

115-
@SuppressLint("InlinedApi")
116-
fun Context.getMediaStoreRecordings(trashed: Boolean = false): ArrayList<Recording> {
101+
@Deprecated(
102+
message = "Use getRecordings instead. This method is only here for backward compatibility.",
103+
replaceWith = ReplaceWith("getRecordings(trashed = true)")
104+
)
105+
private fun Context.getMediaStoreTrashedRecordings(): ArrayList<Recording> {
117106
val recordings = ArrayList<Recording>()
118-
119-
val uri = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
120-
val projection = arrayOf(
121-
Media._ID,
122-
Media.DISPLAY_NAME,
123-
Media.DATE_ADDED,
124-
Media.DURATION,
125-
Media.SIZE
126-
)
127-
128-
var selection = "${Media.OWNER_PACKAGE_NAME} = ?"
129-
var selectionArgs = arrayOf(packageName)
130-
val sortOrder = "${Media.DATE_ADDED} DESC"
131-
132-
if (config.useRecycleBin) {
133-
val trashedValue = if (trashed) 1 else 0
134-
selection += " AND ${Media.IS_TRASHED} = ?"
135-
selectionArgs = selectionArgs.plus(trashedValue.toString())
136-
}
137-
138-
queryCursor(uri, projection, selection, selectionArgs, sortOrder, true) { cursor ->
139-
val recording = readRecordingFromCursor(cursor)
140-
recordings.add(recording)
107+
val folder = config.saveRecordingsFolder
108+
val documentFiles = getDocumentSdk30(folder)?.listFiles() ?: return recordings
109+
documentFiles.forEach { file ->
110+
if (file.isTrashedMediaStoreRecording()) {
111+
val recording = readRecordingFromFile(file)
112+
recordings.add(
113+
recording.copy(
114+
title = "^\\.trashed-\\d+-".toRegex().replace(file.name!!, "")
115+
)
116+
)
117+
}
141118
}
142119

143120
return recordings
144121
}
145122

146-
fun Context.getLegacyRecordings(trashed: Boolean = false): ArrayList<Recording> {
123+
private fun Context.getLegacyRecordings(trashed: Boolean = false): ArrayList<Recording> {
147124
val recordings = ArrayList<Recording>()
148125
val folder = if (trashed) {
149126
trashFolder
@@ -173,106 +150,23 @@ fun Context.getLegacyRecordings(trashed: Boolean = false): ArrayList<Recording>
173150
return recordings
174151
}
175152

176-
fun Context.getSAFRecordings(trashed: Boolean = false): ArrayList<Recording> {
177-
val recordings = ArrayList<Recording>()
178-
val folder = if (trashed) {
179-
trashFolder
180-
} else {
181-
config.saveRecordingsFolder
182-
}
183-
val files = getDocumentSdk30(folder)?.listFiles() ?: return recordings
184-
185-
files.filter { it.type?.startsWith("audio") == true && !it.name.isNullOrEmpty() }.forEach {
186-
val id = it.hashCode()
187-
val title = it.name!!
188-
val path = it.uri.toString()
189-
val timestamp = (it.lastModified() / 1000).toInt()
190-
val duration = getDurationFromUri(it.uri)
191-
val size = it.length().toInt()
192-
recordings.add(
193-
Recording(
194-
id = id,
195-
title = title,
196-
path = path,
197-
timestamp = timestamp,
198-
duration = duration.toInt(),
199-
size = size
200-
)
201-
)
202-
}
203-
204-
recordings.sortByDescending { it.timestamp }
205-
return recordings
206-
}
207-
208-
fun Context.getAllRecordings(trashed: Boolean = false): ArrayList<Recording> {
209-
val recordings = ArrayList<Recording>()
210-
return when {
211-
isRPlus() -> {
212-
recordings.addAll(getNewMediaStoreRecordings(trashed))
213-
recordings.addAll(getSAFRecordings(trashed))
214-
recordings
215-
}
216-
217-
isQPlus() -> {
218-
recordings.addAll(getMediaStoreRecordings(trashed))
219-
recordings.addAll(getLegacyRecordings(trashed))
220-
recordings
221-
}
222-
223-
else -> {
224-
recordings.addAll(getLegacyRecordings(trashed))
225-
recordings
226-
}
227-
}
228-
}
229-
230-
val Context.trashFolder
231-
get() = "${config.saveRecordingsFolder}/.trash"
232-
233-
fun Context.getOrCreateTrashFolder(): String {
234-
val folder = File(trashFolder)
235-
if (!folder.exists()) {
236-
folder.mkdir()
237-
}
238-
return trashFolder
239-
}
240-
241-
private fun Context.readRecordingFromCursor(cursor: Cursor): Recording {
242-
val id = cursor.getIntValue(Media._ID)
243-
val title = cursor.getStringValue(Media.DISPLAY_NAME)
244-
val timestamp = cursor.getIntValue(Media.DATE_ADDED)
245-
var duration = cursor.getLongValue(Media.DURATION) / 1000
246-
var size = cursor.getIntValue(Media.SIZE)
247-
248-
if (duration == 0L) {
249-
duration = getDurationFromUri(getAudioFileContentUri(id.toLong()))
250-
}
251-
252-
if (size == 0) {
253-
size = getSizeFromUri(id.toLong())
254-
}
255-
153+
private fun Context.readRecordingFromFile(file: DocumentFile): Recording {
154+
val id = file.hashCode()
155+
val title = file.name!!
156+
val path = file.uri.toString()
157+
val timestamp = (file.lastModified() / 1000).toInt()
158+
val duration = getDurationFromUri(file.uri)
159+
val size = file.length().toInt()
256160
return Recording(
257161
id = id,
258162
title = title,
259-
path = "",
163+
path = path,
260164
timestamp = timestamp,
261165
duration = duration.toInt(),
262166
size = size
263167
)
264168
}
265169

266-
private fun Context.getSizeFromUri(id: Long): Int {
267-
val recordingUri = getAudioFileContentUri(id)
268-
return try {
269-
contentResolver.openInputStream(recordingUri)
270-
?.use { it.available() } ?: 0
271-
} catch (e: Exception) {
272-
0
273-
}
274-
}
275-
276170
private fun Context.getDurationFromUri(uri: Uri): Long {
277171
return try {
278172
val retriever = MediaMetadataRetriever()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.fossify.voicerecorder.extensions
2+
3+
import androidx.documentfile.provider.DocumentFile
4+
5+
fun DocumentFile.isAudioRecording(): Boolean {
6+
return type.isAudioMimeType() && !name.isNullOrEmpty() && !name!!.startsWith(".")
7+
}
8+
9+
fun DocumentFile.isTrashedMediaStoreRecording(): Boolean {
10+
return type.isAudioMimeType() && !name.isNullOrEmpty() && name!!.startsWith(".trashed-")
11+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.fossify.voicerecorder.extensions
2+
3+
fun String?.isAudioMimeType(): Boolean {
4+
return this?.startsWith("audio") == true
5+
}

‎app/src/main/kotlin/org/fossify/voicerecorder/fragments/PlayerFragment.kt

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,12 @@ import android.content.Context
55
import android.graphics.drawable.Drawable
66
import android.media.AudioAttributes
77
import android.media.MediaPlayer
8-
import android.net.Uri
98
import android.os.Handler
109
import android.os.Looper
1110
import android.os.PowerManager
12-
import android.provider.DocumentsContract
1311
import android.util.AttributeSet
1412
import android.widget.SeekBar
13+
import androidx.core.net.toUri
1514
import org.fossify.commons.extensions.applyColorFilter
1615
import org.fossify.commons.extensions.areSystemAnimationsEnabled
1716
import org.fossify.commons.extensions.beVisibleIf
@@ -30,7 +29,6 @@ import org.fossify.voicerecorder.activities.SimpleActivity
3029
import org.fossify.voicerecorder.adapters.RecordingsAdapter
3130
import org.fossify.voicerecorder.databinding.FragmentPlayerBinding
3231
import org.fossify.voicerecorder.extensions.config
33-
import org.fossify.voicerecorder.helpers.getAudioFileContentUri
3432
import org.fossify.voicerecorder.interfaces.RefreshRecordingsListener
3533
import org.fossify.voicerecorder.models.Events
3634
import org.fossify.voicerecorder.models.Recording
@@ -246,20 +244,7 @@ class PlayerFragment(
246244
reset()
247245

248246
try {
249-
val uri = Uri.parse(recording.path)
250-
when {
251-
DocumentsContract.isDocumentUri(context, uri) -> {
252-
setDataSource(context, uri)
253-
}
254-
255-
recording.path.isEmpty() -> {
256-
setDataSource(context, getAudioFileContentUri(recording.id.toLong()))
257-
}
258-
259-
else -> {
260-
setDataSource(recording.path)
261-
}
262-
}
247+
setDataSource(context, recording.path.toUri())
263248
} catch (e: Exception) {
264249
context?.showErrorToast(e)
265250
return

‎app/src/main/kotlin/org/fossify/voicerecorder/fragments/RecorderFragment.kt

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ import org.fossify.commons.extensions.getFormattedDuration
2020
import org.fossify.commons.extensions.getProperPrimaryColor
2121
import org.fossify.commons.extensions.getProperTextColor
2222
import org.fossify.commons.extensions.openNotificationSettings
23+
import org.fossify.commons.extensions.toast
2324
import org.fossify.voicerecorder.databinding.FragmentRecorderBinding
2425
import org.fossify.voicerecorder.extensions.config
26+
import org.fossify.voicerecorder.extensions.ensureStoragePermission
2527
import org.fossify.voicerecorder.extensions.setDebouncedClickListener
2628
import org.fossify.voicerecorder.helpers.GET_RECORDER_INFO
2729
import org.fossify.voicerecorder.helpers.RECORDING_PAUSED
@@ -74,17 +76,24 @@ class RecorderFragment(
7476

7577
updateRecordingDuration(0)
7678
binding.toggleRecordingButton.setDebouncedClickListener {
77-
(context as? BaseSimpleActivity)?.handleNotificationPermission { granted ->
78-
if (granted) {
79-
toggleRecording()
80-
} else {
81-
PermissionRequiredDialog(
82-
activity = context as BaseSimpleActivity,
83-
textId = org.fossify.commons.R.string.allow_notifications_voice_recorder,
84-
positiveActionCallback = {
85-
(context as BaseSimpleActivity).openNotificationSettings()
79+
val activity = context as? BaseSimpleActivity
80+
activity?.ensureStoragePermission {
81+
if (it) {
82+
activity.handleNotificationPermission { granted ->
83+
if (granted) {
84+
toggleRecording()
85+
} else {
86+
PermissionRequiredDialog(
87+
activity = context as BaseSimpleActivity,
88+
textId = org.fossify.commons.R.string.allow_notifications_voice_recorder,
89+
positiveActionCallback = {
90+
(context as BaseSimpleActivity).openNotificationSettings()
91+
}
92+
)
8693
}
87-
)
94+
}
95+
} else {
96+
activity.toast(org.fossify.commons.R.string.no_storage_permissions)
8897
}
8998
}
9099
}

‎app/src/main/kotlin/org/fossify/voicerecorder/helpers/Constants.kt

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
package org.fossify.voicerecorder.helpers
22

3-
import android.annotation.SuppressLint
4-
import android.content.ContentUris
5-
import android.net.Uri
6-
import android.provider.MediaStore
7-
import android.provider.MediaStore.Audio.Media
8-
import org.fossify.commons.helpers.isQPlus
9-
103
const val REPOSITORY_NAME = "Voice-Recorder"
114

125
const val RECORDER_RUNNING_NOTIF_ID = 10000
@@ -42,13 +35,4 @@ const val USE_RECYCLE_BIN = "use_recycle_bin"
4235
const val LAST_RECYCLE_BIN_CHECK = "last_recycle_bin_check"
4336
const val KEEP_SCREEN_ON = "keep_screen_on"
4437

45-
@SuppressLint("InlinedApi")
46-
fun getAudioFileContentUri(id: Long): Uri {
47-
val baseUri = if (isQPlus()) {
48-
Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
49-
} else {
50-
Media.EXTERNAL_CONTENT_URI
51-
}
52-
53-
return ContentUris.withAppendedId(baseUri, id)
54-
}
38+
const val DEFAULT_RECORDINGS_FOLDER = "Recordings"

‎app/src/main/kotlin/org/fossify/voicerecorder/services/RecorderService.kt

Lines changed: 22 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,20 @@ import android.app.NotificationChannel
66
import android.app.NotificationManager
77
import android.app.PendingIntent
88
import android.app.Service
9-
import android.content.ContentValues
109
import android.content.Context
1110
import android.content.Intent
1211
import android.media.MediaScannerConnection
1312
import android.net.Uri
1413
import android.os.IBinder
15-
import android.provider.MediaStore
16-
import android.provider.MediaStore.Audio.Media
1714
import androidx.core.app.NotificationCompat
1815
import org.fossify.commons.extensions.createDocumentUriUsingFirstParentTreeUri
1916
import org.fossify.commons.extensions.createSAFFileSdk30
2017
import org.fossify.commons.extensions.getCurrentFormattedDateTime
2118
import org.fossify.commons.extensions.getDocumentFile
22-
import org.fossify.commons.extensions.getFileInputStreamSync
2319
import org.fossify.commons.extensions.getFilenameFromPath
2420
import org.fossify.commons.extensions.getLaunchIntent
2521
import org.fossify.commons.extensions.getMimeType
2622
import org.fossify.commons.extensions.getParentPath
27-
import org.fossify.commons.extensions.hasProperStoredFirstParentUri
2823
import org.fossify.commons.extensions.isPathOnSD
2924
import org.fossify.commons.extensions.showErrorToast
3025
import org.fossify.commons.extensions.toast
@@ -33,7 +28,6 @@ import org.fossify.commons.helpers.isRPlus
3328
import org.fossify.voicerecorder.R
3429
import org.fossify.voicerecorder.activities.SplashActivity
3530
import org.fossify.voicerecorder.extensions.config
36-
import org.fossify.voicerecorder.extensions.getDefaultRecordingsRelativePath
3731
import org.fossify.voicerecorder.extensions.updateWidgets
3832
import org.fossify.voicerecorder.helpers.EXTENSION_MP3
3933
import org.fossify.voicerecorder.helpers.GET_RECORDER_INFO
@@ -60,7 +54,7 @@ class RecorderService : Service() {
6054
}
6155

6256

63-
private var currFilePath = ""
57+
private var recordingFile = ""
6458
private var duration = 0
6559
private var status = RECORDING_STOPPED
6660
private var durationTimer = Timer()
@@ -103,14 +97,8 @@ class RecorderService : Service() {
10397
defaultFolder.mkdir()
10498
}
10599

106-
val baseFolder =
107-
if (isRPlus() && !hasProperStoredFirstParentUri(defaultFolder.absolutePath)) {
108-
cacheDir
109-
} else {
110-
defaultFolder.absolutePath
111-
}
112-
113-
currFilePath = "$baseFolder/${getCurrentFormattedDateTime()}.${config.getExtension()}"
100+
val recordingFolder = defaultFolder.absolutePath
101+
recordingFile = "$recordingFolder/${getCurrentFormattedDateTime()}.${config.getExtension()}"
114102

115103
try {
116104
recorder = if (recordMp3()) {
@@ -119,24 +107,24 @@ class RecorderService : Service() {
119107
MediaRecorderWrapper(this)
120108
}
121109

122-
if (isRPlus() && hasProperStoredFirstParentUri(currFilePath)) {
123-
val fileUri = createDocumentUriUsingFirstParentTreeUri(currFilePath)
124-
createSAFFileSdk30(currFilePath)
110+
if (isRPlus()) {
111+
val fileUri = createDocumentUriUsingFirstParentTreeUri(recordingFile)
112+
createSAFFileSdk30(recordingFile)
125113

126114
val outputFileDescriptor =
127115
contentResolver.openFileDescriptor(fileUri, "w")!!.fileDescriptor
128116

129117
recorder?.setOutputFile(outputFileDescriptor)
130-
} else if (!isRPlus() && isPathOnSD(currFilePath)) {
131-
var document = getDocumentFile(currFilePath.getParentPath())
132-
document = document?.createFile("", currFilePath.getFilenameFromPath())
118+
} else if (isPathOnSD(recordingFile)) {
119+
var document = getDocumentFile(recordingFile.getParentPath())
120+
document = document?.createFile("", recordingFile.getFilenameFromPath())
133121

134122
val outputFileDescriptor =
135123
contentResolver.openFileDescriptor(document!!.uri, "w")!!.fileDescriptor
136124

137125
recorder?.setOutputFile(outputFileDescriptor)
138126
} else {
139-
recorder?.setOutputFile(currFilePath)
127+
recorder?.setOutputFile(recordingFile)
140128
}
141129

142130
recorder?.prepare()
@@ -165,13 +153,8 @@ class RecorderService : Service() {
165153
try {
166154
stop()
167155
release()
168-
169156
ensureBackgroundThread {
170-
if (isRPlus() && !hasProperStoredFirstParentUri(currFilePath)) {
171-
addFileInNewMediaStore()
172-
} else {
173-
addFileInLegacyMediaStore()
174-
}
157+
scanRecording()
175158
EventBus.getDefault().post(Events.RecordingCompleted())
176159
}
177160
} catch (e: RuntimeException) {
@@ -213,40 +196,19 @@ class RecorderService : Service() {
213196
}
214197
}
215198

216-
@SuppressLint("InlinedApi")
217-
private fun addFileInNewMediaStore() {
218-
val audioCollection = Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
219-
val storeFilename = currFilePath.getFilenameFromPath()
220-
221-
val newSongDetails = ContentValues().apply {
222-
put(Media.DISPLAY_NAME, storeFilename)
223-
put(Media.TITLE, storeFilename)
224-
put(Media.MIME_TYPE, storeFilename.getMimeType())
225-
put(Media.RELATIVE_PATH, getDefaultRecordingsRelativePath())
226-
}
227-
228-
val newUri = contentResolver.insert(audioCollection, newSongDetails)
229-
if (newUri == null) {
230-
toast(org.fossify.commons.R.string.unknown_error_occurred)
231-
return
232-
}
233-
234-
try {
235-
val outputStream = contentResolver.openOutputStream(newUri)
236-
val inputStream = getFileInputStreamSync(currFilePath)
237-
inputStream!!.copyTo(outputStream!!, DEFAULT_BUFFER_SIZE)
238-
recordingSavedSuccessfully(newUri)
239-
} catch (e: Exception) {
240-
showErrorToast(e)
241-
}
242-
}
243-
244-
private fun addFileInLegacyMediaStore() {
199+
private fun scanRecording() {
245200
MediaScannerConnection.scanFile(
246201
this,
247-
arrayOf(currFilePath),
248-
arrayOf(currFilePath.getMimeType())
249-
) { _, uri -> recordingSavedSuccessfully(uri) }
202+
arrayOf(recordingFile),
203+
arrayOf(recordingFile.getMimeType())
204+
) { _, uri ->
205+
if (uri == null) {
206+
toast(org.fossify.commons.R.string.unknown_error_occurred)
207+
return@scanFile
208+
}
209+
210+
recordingSavedSuccessfully(uri)
211+
}
250212
}
251213

252214
private fun recordingSavedSuccessfully(savedUri: Uri) {

‎app/src/main/res/values/strings.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources>
33
<string name="app_launcher_name">Voice Recorder</string>
4+
<string name="confirm_recording_folder">You must confirm the folder where you want to save your recordings. Please press "OK" to continue.</string>
45
<string name="recording_saved_successfully">Recording saved successfully</string>
56
<string name="recording_too_short">Recording was too short to record!</string>
67
<string name="recording">Recording</string>

‎gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ androidx-swiperefreshlayout = "1.1.0"
1313
eventbus = "3.3.1"
1414
#Fossify
1515
#noinspection GradleDependency
16-
commons = "4013116a24"
16+
commons = "1d71c8a2e8"
1717
#AudioRecordView
1818
audiorecordview = "1.0.4"
1919
#TAndroidLame

0 commit comments

Comments
 (0)
Please sign in to comment.