Merge pull request #107 from KryptKode/feat/customise_recordings_path

allow users to customise recording folder
This commit is contained in:
Tibor Kaputa 2022-04-18 10:50:18 +02:00 committed by GitHub
commit 2073dbb9cc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 107 additions and 34 deletions

View File

@ -62,7 +62,7 @@ android {
} }
dependencies { dependencies {
implementation 'com.github.SimpleMobileTools:Simple-Commons:e3376e4f56' implementation 'com.github.SimpleMobileTools:Simple-Commons:202656a071'
implementation 'org.greenrobot:eventbus:3.2.0' implementation 'org.greenrobot:eventbus:3.2.0'
implementation 'com.github.Armen101:AudioRecordView:1.0.4' implementation 'com.github.Armen101:AudioRecordView:1.0.4'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'

View File

@ -9,7 +9,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission <uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" /> android:maxSdkVersion="29" />
<uses-feature <uses-feature
android:name="android.hardware.faketouch" android:name="android.hardware.faketouch"

View File

@ -82,7 +82,7 @@ class MainActivity : SimpleActivity() {
} }
private fun tryInitVoiceRecorder() { private fun tryInitVoiceRecorder() {
if (isQPlus()) { if (isRPlus()) {
setupViewPager() setupViewPager()
} else { } else {
handlePermission(PERMISSION_WRITE_STORAGE) { handlePermission(PERMISSION_WRITE_STORAGE) {

View File

@ -99,13 +99,20 @@ class SettingsActivity : SimpleActivity() {
} }
private fun setupSaveRecordingsFolder() { private fun setupSaveRecordingsFolder() {
settings_save_recordings_holder.beGoneIf(isQPlus())
settings_save_recordings.text = humanizePath(config.saveRecordingsFolder) settings_save_recordings.text = humanizePath(config.saveRecordingsFolder)
settings_save_recordings_holder.setOnClickListener { settings_save_recordings_holder.setOnClickListener {
FilePickerDialog(this, config.saveRecordingsFolder, false, showFAB = true) { FilePickerDialog(this, config.saveRecordingsFolder, false, showFAB = true) {
val path = it val path = it
handleSAFDialog(it) { handleSAFDialog(path) { grantedSAF ->
if (it) { if (!grantedSAF) {
return@handleSAFDialog
}
handleSAFDialogSdk30(path) { grantedSAF30 ->
if (!grantedSAF30) {
return@handleSAFDialogSdk30
}
config.saveRecordingsFolder = path config.saveRecordingsFolder = path
settings_save_recordings.text = humanizePath(config.saveRecordingsFolder) settings_save_recordings.text = humanizePath(config.saveRecordingsFolder)
} }

View File

@ -116,10 +116,10 @@ class RecordingsAdapter(
private fun shareRecordings() { private fun shareRecordings() {
val selectedItems = getSelectedItems() val selectedItems = getSelectedItems()
val paths = if (isQPlus()) { val paths = selectedItems.map {
selectedItems.map { getAudioFileContentUri(it.id.toLong()).toString() } it.path.ifEmpty {
} else { getAudioFileContentUri(it.id.toLong()).toString()
selectedItems.map { it.path } }
} }
activity.sharePathsIntent(paths, BuildConfig.APPLICATION_ID) activity.sharePathsIntent(paths, BuildConfig.APPLICATION_ID)

View File

@ -7,6 +7,10 @@ import android.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Canvas import android.graphics.Canvas
import android.graphics.drawable.Drawable 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.Config
import com.simplemobiletools.voicerecorder.helpers.IS_RECORDING import com.simplemobiletools.voicerecorder.helpers.IS_RECORDING
import com.simplemobiletools.voicerecorder.helpers.MyWidgetRecordDisplayProvider 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)
}
}

View File

@ -6,15 +6,18 @@ import android.graphics.drawable.Drawable
import android.media.AudioManager import android.media.AudioManager
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.media.MediaPlayer import android.media.MediaPlayer
import android.net.Uri
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.PowerManager import android.os.PowerManager
import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media import android.provider.MediaStore.Audio.Media
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.SeekBar import android.widget.SeekBar
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.isQPlus import com.simplemobiletools.commons.helpers.isQPlus
import com.simplemobiletools.commons.helpers.isRPlus
import com.simplemobiletools.voicerecorder.R import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.activities.SimpleActivity import com.simplemobiletools.voicerecorder.activities.SimpleActivity
import com.simplemobiletools.voicerecorder.adapters.RecordingsAdapter import com.simplemobiletools.voicerecorder.adapters.RecordingsAdapter
@ -29,6 +32,8 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import java.io.File import java.io.File
import java.util.* import java.util.*
import kotlin.collections.ArrayList
import kotlin.math.roundToLong
class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener { class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPagerFragment(context, attributeSet), RefreshRecordingsListener {
private val FAST_FORWARD_SKIP_MS = 10000 private val FAST_FORWARD_SKIP_MS = 10000
@ -162,10 +167,24 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
} }
private fun getRecordings(): ArrayList<Recording> { private fun getRecordings(): ArrayList<Recording> {
return if (isQPlus()) { val recordings = ArrayList<Recording>()
getMediaStoreRecordings() return when {
} else { isRPlus() -> {
getLegacyRecordings() 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) var size = cursor.getIntValue(Media.SIZE)
if (duration == 0L) { if (duration == 0L) {
duration = getDurationFromUri(id.toLong()) duration = getDurationFromUri(getAudioFileContentUri(id.toLong()))
} }
if (size == 0) { if (size == 0) {
@ -222,17 +241,34 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
val recording = Recording(id, title, path, timestamp, duration, size) val recording = Recording(id, title, path, timestamp, duration, size)
recordings.add(recording) recordings.add(recording)
} }
return recordings
}
private fun getSAFRecordings(): ArrayList<Recording> {
val recordings = ArrayList<Recording>()
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 } recordings.sortByDescending { it.timestamp }
return recordings return recordings
} }
private fun getDurationFromUri(id: Long): Long { private fun getDurationFromUri(uri: Uri): Long {
return try { return try {
val retriever = MediaMetadataRetriever() val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, getAudioFileContentUri(id)) retriever.setDataSource(context, uri)
val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)!! val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)!!
Math.round(time.toLong() / 1000.toDouble()) (time.toLong() / 1000.toDouble()).roundToLong()
} catch (e: Exception) { } catch (e: Exception) {
0L 0L
} }
@ -279,10 +315,17 @@ class PlayerFragment(context: Context, attributeSet: AttributeSet) : MyViewPager
reset() reset()
try { try {
if (isQPlus()) { val uri = Uri.parse(recording.path)
setDataSource(context, getAudioFileContentUri(recording.id.toLong())) when {
} else { DocumentsContract.isDocumentUri(context, uri) -> {
setDataSource(recording.path) setDataSource(context, uri)
}
recording.path.isEmpty() -> {
setDataSource(context, getAudioFileContentUri(recording.id.toLong()))
}
else -> {
setDataSource(recording.path)
}
} }
} catch (e: Exception) { } catch (e: Exception) {
context?.showErrorToast(e) context?.showErrorToast(e)

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.media.MediaRecorder import android.media.MediaRecorder
import com.simplemobiletools.commons.helpers.BaseConfig import com.simplemobiletools.commons.helpers.BaseConfig
import com.simplemobiletools.voicerecorder.R import com.simplemobiletools.voicerecorder.R
import com.simplemobiletools.voicerecorder.extensions.getDefaultRecordingsFolder
class Config(context: Context) : BaseConfig(context) { class Config(context: Context) : BaseConfig(context) {
companion object { companion object {
@ -15,7 +16,7 @@ class Config(context: Context) : BaseConfig(context) {
set(hideNotification) = prefs.edit().putBoolean(HIDE_NOTIFICATION, hideNotification).apply() set(hideNotification) = prefs.edit().putBoolean(HIDE_NOTIFICATION, hideNotification).apply()
var saveRecordingsFolder: String 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() set(saveRecordingsFolder) = prefs.edit().putString(SAVE_RECORDINGS, saveRecordingsFolder).apply()
var extension: Int var extension: Int

View File

@ -9,7 +9,6 @@ import android.content.Intent
import android.media.MediaRecorder import android.media.MediaRecorder
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.os.Build import android.os.Build
import android.os.Environment
import android.os.IBinder import android.os.IBinder
import android.provider.MediaStore import android.provider.MediaStore
import android.provider.MediaStore.Audio.Media import android.provider.MediaStore.Audio.Media
@ -17,10 +16,11 @@ import androidx.core.app.NotificationCompat
import com.simplemobiletools.commons.extensions.* import com.simplemobiletools.commons.extensions.*
import com.simplemobiletools.commons.helpers.ensureBackgroundThread import com.simplemobiletools.commons.helpers.ensureBackgroundThread
import com.simplemobiletools.commons.helpers.isOreoPlus 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.R
import com.simplemobiletools.voicerecorder.activities.SplashActivity import com.simplemobiletools.voicerecorder.activities.SplashActivity
import com.simplemobiletools.voicerecorder.extensions.config import com.simplemobiletools.voicerecorder.extensions.config
import com.simplemobiletools.voicerecorder.extensions.getDefaultRecordingsRelativePath
import com.simplemobiletools.voicerecorder.extensions.updateWidgets import com.simplemobiletools.voicerecorder.extensions.updateWidgets
import com.simplemobiletools.voicerecorder.helpers.* import com.simplemobiletools.voicerecorder.helpers.*
import com.simplemobiletools.voicerecorder.models.Events import com.simplemobiletools.voicerecorder.models.Events
@ -72,14 +72,14 @@ class RecorderService : Service() {
return return
} }
val baseFolder = if (isQPlus()) { val defaultFolder = File(config.saveRecordingsFolder)
if (!defaultFolder.exists()) {
defaultFolder.mkdir()
}
val baseFolder = if (isRPlus() && !hasProperStoredFirstParentUri(defaultFolder.absolutePath)) {
cacheDir cacheDir
} else { } else {
val defaultFolder = File(config.saveRecordingsFolder)
if (!defaultFolder.exists()) {
defaultFolder.mkdir()
}
defaultFolder.absolutePath defaultFolder.absolutePath
} }
@ -93,7 +93,12 @@ class RecorderService : Service() {
setAudioEncodingBitRate(config.bitrate) setAudioEncodingBitRate(config.bitrate)
setAudioSamplingRate(44100) 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()) var document = getDocumentFile(currFilePath.getParentPath())
document = document?.createFile("", currFilePath.getFilenameFromPath()) document = document?.createFile("", currFilePath.getFilenameFromPath())
@ -132,7 +137,7 @@ class RecorderService : Service() {
release() release()
ensureBackgroundThread { ensureBackgroundThread {
if (isQPlus()) { if (isRPlus() && !hasProperStoredFirstParentUri(currFilePath) ) {
addFileInNewMediaStore() addFileInNewMediaStore()
} else { } else {
addFileInLegacyMediaStore() addFileInLegacyMediaStore()
@ -184,7 +189,7 @@ class RecorderService : Service() {
put(Media.DISPLAY_NAME, storeFilename) put(Media.DISPLAY_NAME, storeFilename)
put(Media.TITLE, storeFilename) put(Media.TITLE, storeFilename)
put(Media.MIME_TYPE, storeFilename.getMimeType()) 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) val newUri = contentResolver.insert(audioCollection, newSongDetails)