diff --git a/app/build.gradle b/app/build.gradle
index 7df722d..542a273 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -62,7 +62,7 @@ android {
}
dependencies {
- implementation 'com.github.SimpleMobileTools:Simple-Commons:e3376e4f56'
+ implementation 'com.github.SimpleMobileTools:Simple-Commons:202656a071'
implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'com.github.Armen101:AudioRecordView:1.0.4'
implementation 'androidx.documentfile:documentfile:1.0.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index d18255f..066c962 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,7 +9,7 @@
+ android:maxSdkVersion="29" />
+ if (!grantedSAF) {
+ return@handleSAFDialog
+ }
+
+ handleSAFDialogSdk30(path) { grantedSAF30 ->
+ if (!grantedSAF30) {
+ return@handleSAFDialogSdk30
+ }
+
config.saveRecordingsFolder = path
settings_save_recordings.text = humanizePath(config.saveRecordingsFolder)
}
diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt
index 1da113b..0caa620 100644
--- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/adapters/RecordingsAdapter.kt
@@ -116,10 +116,10 @@ class RecordingsAdapter(
private fun shareRecordings() {
val selectedItems = getSelectedItems()
- val paths = if (isQPlus()) {
- selectedItems.map { getAudioFileContentUri(it.id.toLong()).toString() }
- } else {
- selectedItems.map { it.path }
+ val paths = selectedItems.map {
+ it.path.ifEmpty {
+ getAudioFileContentUri(it.id.toLong()).toString()
+ }
}
activity.sharePathsIntent(paths, BuildConfig.APPLICATION_ID)
diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Context.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Context.kt
index b5f88f1..17067f1 100644
--- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Context.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/extensions/Context.kt
@@ -7,6 +7,10 @@ import android.content.Intent
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.Drawable
+import android.os.Environment
+import com.simplemobiletools.commons.extensions.internalStoragePath
+import com.simplemobiletools.commons.helpers.isQPlus
+import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.helpers.Config
import com.simplemobiletools.voicerecorder.helpers.IS_RECORDING
import com.simplemobiletools.voicerecorder.helpers.MyWidgetRecordDisplayProvider
@@ -34,3 +38,16 @@ fun Context.updateWidgets(isRecording: Boolean) {
}
}
}
+
+fun Context.getDefaultRecordingsFolder(): String {
+ val defaultPath = getDefaultRecordingsRelativePath()
+ return "$internalStoragePath/$defaultPath"
+}
+
+fun Context.getDefaultRecordingsRelativePath(): String {
+ return if (isQPlus()) {
+ "${Environment.DIRECTORY_MUSIC}/Recordings"
+ } else {
+ getString(R.string.app_name)
+ }
+}
diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt
index f2546ca..21e821b 100644
--- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/fragments/PlayerFragment.kt
@@ -6,15 +6,18 @@ import android.graphics.drawable.Drawable
import android.media.AudioManager
import android.media.MediaMetadataRetriever
import android.media.MediaPlayer
+import android.net.Uri
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
+import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media
import android.util.AttributeSet
import android.widget.SeekBar
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isQPlus
+import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.SimpleActivity
import com.simplemobiletools.voicerecorder.adapters.RecordingsAdapter
@@ -29,6 +32,8 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.File
import java.util.*
+import kotlin.collections.ArrayList
+import kotlin.math.roundToLong
class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener {
private val FAST_FORWARD_SKIP_MS = 10000
@@ -162,10 +167,24 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
}
private fun getRecordings(): ArrayList {
- return if (isQPlus()) {
- getMediaStoreRecordings()
- } else {
- getLegacyRecordings()
+ val recordings = ArrayList()
+ return when {
+ isRPlus() -> {
+ recordings.addAll(getMediaStoreRecordings())
+ recordings.addAll(getSAFRecordings())
+ recordings
+ }
+ isQPlus() -> {
+ recordings.addAll(getMediaStoreRecordings())
+ recordings.addAll(getLegacyRecordings())
+ recordings
+ }
+ else -> {
+ recordings.addAll(getLegacyRecordings())
+ recordings
+ }
+ }.apply {
+ sortByDescending { it.timestamp }
}
}
@@ -194,7 +213,7 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
var size = cursor.getIntValue(Media.SIZE)
if (duration == 0L) {
- duration = getDurationFromUri(id.toLong())
+ duration = getDurationFromUri(getAudioFileContentUri(id.toLong()))
}
if (size == 0) {
@@ -222,17 +241,34 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
val recording = Recording(id, title, path, timestamp, duration, size)
recordings.add(recording)
}
+ return recordings
+ }
+
+ private fun getSAFRecordings(): ArrayList {
+ val recordings = ArrayList()
+ val files = context.getDocumentSdk30(context.config.saveRecordingsFolder)?.listFiles() ?: return recordings
+
+ files.filter { it.type?.startsWith("audio") == true && !it.name.isNullOrEmpty() }.forEach {
+ val id = it.hashCode()
+ val title = it.name!!
+ val path = it.uri.toString()
+ val timestamp = (it.lastModified() / 1000).toInt()
+ val duration = getDurationFromUri(it.uri)
+ val size = it.length().toInt()
+ val recording = Recording(id, title, path, timestamp, duration.toInt(), size)
+ recordings.add(recording)
+ }
recordings.sortByDescending { it.timestamp }
return recordings
}
- private fun getDurationFromUri(id: Long): Long {
+ private fun getDurationFromUri(uri: Uri): Long {
return try {
val retriever = MediaMetadataRetriever()
- retriever.setDataSource(context, getAudioFileContentUri(id))
+ retriever.setDataSource(context, uri)
val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)!!
- Math.round(time.toLong() / 1000.toDouble())
+ (time.toLong() / 1000.toDouble()).roundToLong()
} catch (e: Exception) {
0L
}
@@ -279,10 +315,17 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
reset()
try {
- if (isQPlus()) {
- setDataSource(context, getAudioFileContentUri(recording.id.toLong()))
- } else {
- setDataSource(recording.path)
+ val uri = Uri.parse(recording.path)
+ when {
+ DocumentsContract.isDocumentUri(context, uri) -> {
+ setDataSource(context, uri)
+ }
+ recording.path.isEmpty() -> {
+ setDataSource(context, getAudioFileContentUri(recording.id.toLong()))
+ }
+ else -> {
+ setDataSource(recording.path)
+ }
}
} catch (e: Exception) {
context?.showErrorToast(e)
diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Config.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Config.kt
index 7bca16c..63e7f30 100644
--- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Config.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/helpers/Config.kt
@@ -4,6 +4,7 @@ import android.content.Context
import android.media.MediaRecorder
import com.simplemobiletools.commons.helpers.BaseConfig
import com.simplemobiletools.voicerecorder.R
+import com.simplemobiletools.voicerecorder.extensions.getDefaultRecordingsFolder
class Config(context: Context) : BaseConfig(context) {
companion object {
@@ -15,7 +16,7 @@ class Config(context: Context) : BaseConfig(context) {
set(hideNotification) = prefs.edit().putBoolean(HIDE_NOTIFICATION, hideNotification).apply()
var saveRecordingsFolder: String
- get() = prefs.getString(SAVE_RECORDINGS, "$internalStoragePath/${context.getString(R.string.app_name)}")!!
+ get() = prefs.getString(SAVE_RECORDINGS, context.getDefaultRecordingsFolder())!!
set(saveRecordingsFolder) = prefs.edit().putString(SAVE_RECORDINGS, saveRecordingsFolder).apply()
var extension: Int
diff --git a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/services/RecorderService.kt b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/services/RecorderService.kt
index 0cf7b19..f28aa34 100644
--- a/app/src/main/kotlin/com/simplemobiletools/voicerecorder/services/RecorderService.kt
+++ b/app/src/main/kotlin/com/simplemobiletools/voicerecorder/services/RecorderService.kt
@@ -9,7 +9,6 @@ import android.content.Intent
import android.media.MediaRecorder
import android.media.MediaScannerConnection
import android.os.Build
-import android.os.Environment
import android.os.IBinder
import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media
@@ -17,10 +16,11 @@ import androidx.core.app.NotificationCompat
import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.helpers.isOreoPlus
-import com.simplemobiletools.commons.helpers.isQPlus
+import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.SplashActivity
import com.simplemobiletools.voicerecorder.extensions.config
+import com.simplemobiletools.voicerecorder.extensions.getDefaultRecordingsRelativePath
import com.simplemobiletools.voicerecorder.extensions.updateWidgets
import com.simplemobiletools.voicerecorder.helpers.*
import com.simplemobiletools.voicerecorder.models.Events
@@ -72,14 +72,14 @@ class RecorderService : Service() {
return
}
- val baseFolder = if (isQPlus()) {
+ val defaultFolder = File(config.saveRecordingsFolder)
+ if (!defaultFolder.exists()) {
+ defaultFolder.mkdir()
+ }
+
+ val baseFolder = if (isRPlus() && !hasProperStoredFirstParentUri(defaultFolder.absolutePath)) {
cacheDir
} else {
- val defaultFolder = File(config.saveRecordingsFolder)
- if (!defaultFolder.exists()) {
- defaultFolder.mkdir()
- }
-
defaultFolder.absolutePath
}
@@ -93,7 +93,12 @@ class RecorderService : Service() {
setAudioEncodingBitRate(config.bitrate)
setAudioSamplingRate(44100)
- if (!isQPlus() && isPathOnSD(currFilePath)) {
+ if (isRPlus() && hasProperStoredFirstParentUri(currFilePath)) {
+ val fileUri = createDocumentUriUsingFirstParentTreeUri(currFilePath)
+ createSAFFileSdk30(currFilePath)
+ val outputFileDescriptor = contentResolver.openFileDescriptor(fileUri, "w")!!.fileDescriptor
+ setOutputFile(outputFileDescriptor)
+ } else if (!isRPlus() && isPathOnSD(currFilePath)) {
var document = getDocumentFile(currFilePath.getParentPath())
document = document?.createFile("", currFilePath.getFilenameFromPath())
@@ -132,7 +137,7 @@ class RecorderService : Service() {
release()
ensureBackgroundThread {
- if (isQPlus()) {
+ if (isRPlus() && !hasProperStoredFirstParentUri(currFilePath) ) {
addFileInNewMediaStore()
} else {
addFileInLegacyMediaStore()
@@ -184,7 +189,7 @@ class RecorderService : Service() {
put(Media.DISPLAY_NAME, storeFilename)
put(Media.TITLE, storeFilename)
put(Media.MIME_TYPE, storeFilename.getMimeType())
- put(Media.RELATIVE_PATH, "${Environment.DIRECTORY_MUSIC}/Recordings")
+ put(Media.RELATIVE_PATH, getDefaultRecordingsRelativePath())
}
val newUri = contentResolver.insert(audioCollection, newSongDetails)