Merge pull request #107 from KryptKode/feat/customise_recordings_path
allow users to customise recording folder
This commit is contained in:
commit
2073dbb9cc
|
@ -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'
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue