fix: Fix legacy storage support

This commit is contained in:
Myzel394 2023-12-31 15:49:28 +01:00
parent ef6487903e
commit 4681a1d924
No known key found for this signature in database
GPG Key ID: 79CC92F37B3E1A2B
6 changed files with 163 additions and 82 deletions

View File

@ -4,9 +4,12 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.net.toFile
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import app.myzel394.alibi.helpers.MediaConverter.Companion.concatenateVideoFiles import app.myzel394.alibi.helpers.MediaConverter.Companion.concatenateVideoFiles
import app.myzel394.alibi.helpers.VideoBatchesFolder.Companion.MEDIA_SUBFOLDER
import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.FFmpegKitConfig
import java.io.File
import java.io.FileDescriptor import java.io.FileDescriptor
import java.time.LocalDateTime import java.time.LocalDateTime
@ -23,7 +26,11 @@ class AudioBatchesFolder(
) { ) {
override val concatenationFunction = ::concatenateVideoFiles override val concatenationFunction = ::concatenateVideoFiles
override val ffmpegParameters = FFMPEG_PARAMETERS override val ffmpegParameters = FFMPEG_PARAMETERS
override val mediaContentUri: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI override val scopedMediaContentUri: Uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
override val legacyMediaFolder = File(
scopedMediaContentUri.toFile(),
MEDIA_SUBFOLDER + "/" + subfolderName
)
private var customFileFileDescriptor: ParcelFileDescriptor? = null private var customFileFileDescriptor: ParcelFileDescriptor? = null

View File

@ -16,8 +16,11 @@ import java.time.LocalDateTime
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.FFmpegKitConfig
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.net.toFile
import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlin.reflect.KFunction3 import kotlin.reflect.KFunction3
@ -29,7 +32,8 @@ abstract class BatchesFolder(
) { ) {
abstract val concatenationFunction: KFunction3<Iterable<String>, String, String, CompletableDeferred<Unit>> abstract val concatenationFunction: KFunction3<Iterable<String>, String, String, CompletableDeferred<Unit>>
abstract val ffmpegParameters: Array<String> abstract val ffmpegParameters: Array<String>
abstract val mediaContentUri: Uri abstract val scopedMediaContentUri: Uri
abstract val legacyMediaFolder: File
val mediaPrefix val mediaPrefix
get() = MEDIA_RECORDINGS_PREFIX + subfolderName.substring(1) + "-" get() = MEDIA_RECORDINGS_PREFIX + subfolderName.substring(1) + "-"
@ -45,7 +49,11 @@ abstract class BatchesFolder(
} }
BatchType.MEDIA -> { BatchType.MEDIA -> {
// Add support for < Android 10 // Scoped storage works fine on new Android versions,
// we need to manually manage the folder on older versions
if (!SUPPORTS_SCOPED_STORAGE) {
legacyMediaFolder.mkdirs()
}
} }
} }
} }
@ -58,11 +66,12 @@ abstract class BatchesFolder(
return customFolder!!.findFile(subfolderName)!! return customFolder!!.findFile(subfolderName)!!
} }
@RequiresApi(Build.VERSION_CODES.Q)
protected fun queryMediaContent( protected fun queryMediaContent(
callback: (rawName: String, counter: Int, uri: Uri, cursor: Cursor) -> Any?, callback: (rawName: String, counter: Int, uri: Uri, cursor: Cursor) -> Any?,
) { ) {
context.contentResolver.query( context.contentResolver.query(
mediaContentUri, scopedMediaContentUri,
null, null,
null, null,
null, null,
@ -89,7 +98,7 @@ abstract class BatchesFolder(
continue continue
} }
val uri = Uri.withAppendedPath(mediaContentUri, id) val uri = Uri.withAppendedPath(scopedMediaContentUri, id)
val result = callback(rawName, counter, uri, cursor) val result = callback(rawName, counter, uri, cursor)
@ -127,6 +136,7 @@ abstract class BatchesFolder(
BatchType.MEDIA -> { BatchType.MEDIA -> {
val filePaths = mutableListOf<String>() val filePaths = mutableListOf<String>()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
queryMediaContent { _, _, uri, _ -> queryMediaContent { _, _, uri, _ ->
filePaths.add( filePaths.add(
FFmpegKitConfig.getSafParameterForRead( FFmpegKitConfig.getSafParameterForRead(
@ -135,6 +145,11 @@ abstract class BatchesFolder(
)!! )!!
) )
} }
} else {
legacyMediaFolder.listFiles()?.forEach {
filePaths.add(it.absolutePath)
}
}
filePaths filePaths
} }
@ -175,6 +190,7 @@ abstract class BatchesFolder(
BatchType.MEDIA -> { BatchType.MEDIA -> {
var exists = false var exists = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
queryMediaContent { rawName, _, _, _ -> queryMediaContent { rawName, _, _, _ ->
if (rawName == fileName) { if (rawName == fileName) {
exists = true exists = true
@ -183,7 +199,13 @@ abstract class BatchesFolder(
} }
} }
exists return exists
} else {
return File(
legacyMediaFolder,
fileName,
).exists()
}
} }
} }
} }
@ -251,6 +273,7 @@ abstract class BatchesFolder(
} }
BatchType.MEDIA -> { BatchType.MEDIA -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
queryMediaContent { _, _, uri, _ -> queryMediaContent { _, _, uri, _ ->
context.contentResolver.delete( context.contentResolver.delete(
uri, uri,
@ -258,6 +281,9 @@ abstract class BatchesFolder(
null, null,
) )
} }
} else {
legacyMediaFolder.deleteRecursively()
}
} }
} }
} }
@ -272,12 +298,16 @@ abstract class BatchesFolder(
BatchType.MEDIA -> { BatchType.MEDIA -> {
var hasRecordings = false var hasRecordings = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
queryMediaContent { _, _, _, _ -> queryMediaContent { _, _, _, _ ->
hasRecordings = true hasRecordings = true
return@queryMediaContent true return@queryMediaContent true
} }
hasRecordings return hasRecordings
} else {
return legacyMediaFolder.listFiles()?.isNotEmpty() ?: false
}
} }
} }
} }
@ -301,6 +331,7 @@ abstract class BatchesFolder(
} }
BatchType.MEDIA -> { BatchType.MEDIA -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
queryMediaContent { _, counter, uri, _ -> queryMediaContent { _, counter, uri, _ ->
if (counter < earliestCounter) { if (counter < earliestCounter) {
context.contentResolver.delete( context.contentResolver.delete(
@ -310,6 +341,15 @@ abstract class BatchesFolder(
) )
} }
} }
} else {
legacyMediaFolder.listFiles()?.forEach {
val fileCounter = it.nameWithoutExtension.toIntOrNull() ?: return@forEach
if (fileCounter < earliestCounter) {
it.delete()
}
}
}
} }
} }
} }
@ -327,6 +367,7 @@ abstract class BatchesFolder(
return File(getInternalFolder(), "$counter.$fileExtension") return File(getInternalFolder(), "$counter.$fileExtension")
} }
@RequiresApi(Build.VERSION_CODES.Q)
fun getOrCreateMediaFile( fun getOrCreateMediaFile(
name: String, name: String,
mimeType: String, mimeType: String,
@ -336,7 +377,7 @@ abstract class BatchesFolder(
var uri: Uri? = null var uri: Uri? = null
context.contentResolver.query( context.contentResolver.query(
mediaContentUri, scopedMediaContentUri,
arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME), arrayOf(MediaStore.MediaColumns._ID, MediaStore.MediaColumns.DISPLAY_NAME),
"${MediaStore.MediaColumns.DISPLAY_NAME} = '$name'", "${MediaStore.MediaColumns.DISPLAY_NAME} = '$name'",
null, null,
@ -351,7 +392,7 @@ abstract class BatchesFolder(
} }
uri = ContentUris.withAppendedId( uri = ContentUris.withAppendedId(
mediaContentUri, scopedMediaContentUri,
cursor.getLong(id) cursor.getLong(id)
) )
} }
@ -360,7 +401,7 @@ abstract class BatchesFolder(
if (uri == null) { if (uri == null) {
// Create empty output file to be able to write to it // Create empty output file to be able to write to it
uri = context.contentResolver.insert( uri = context.contentResolver.insert(
mediaContentUri, scopedMediaContentUri,
ContentValues().apply { ContentValues().apply {
put( put(
MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.DISPLAY_NAME,

View File

@ -2,6 +2,7 @@ package app.myzel394.alibi.helpers
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.provider.MediaStore import android.provider.MediaStore
@ -10,6 +11,7 @@ import app.myzel394.alibi.helpers.MediaConverter.Companion.concatenateVideoFiles
import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE import app.myzel394.alibi.ui.RECORDER_INTERNAL_SELECTED_VALUE
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
import com.arthenica.ffmpegkit.FFmpegKitConfig import com.arthenica.ffmpegkit.FFmpegKitConfig
import java.io.File
import java.time.LocalDateTime import java.time.LocalDateTime
class VideoBatchesFolder( class VideoBatchesFolder(
@ -25,7 +27,11 @@ class VideoBatchesFolder(
) { ) {
override val concatenationFunction = ::concatenateVideoFiles override val concatenationFunction = ::concatenateVideoFiles
override val ffmpegParameters = FFMPEG_PARAMETERS override val ffmpegParameters = FFMPEG_PARAMETERS
override val mediaContentUri: Uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI override val scopedMediaContentUri: Uri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
override val legacyMediaFolder = File(
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
MEDIA_SUBFOLDER
)
private var customParcelFileDescriptor: ParcelFileDescriptor? = null private var customParcelFileDescriptor: ParcelFileDescriptor? = null
@ -46,16 +52,25 @@ class VideoBatchesFolder(
} }
BatchType.MEDIA -> { BatchType.MEDIA -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val mediaUri = getOrCreateMediaFile( val mediaUri = getOrCreateMediaFile(
name = getName(date, extension), name = getName(date, extension),
mimeType = "video/$extension", mimeType = "video/$extension",
relativePath = MEDIA_RELATIVE_PATH, relativePath = SCOPED_STORAGE_RELATIVE_PATH,
) )
FFmpegKitConfig.getSafParameterForWrite( return FFmpegKitConfig.getSafParameterForWrite(
context, context,
mediaUri mediaUri
)!! )!!
} else {
return File(
legacyMediaFolder.parentFile!!,
getName(date, extension)
).apply {
createNewFile()
}.absolutePath
}
} }
} }
} }
@ -105,7 +120,8 @@ class VideoBatchesFolder(
) )
} }
val MEDIA_RELATIVE_PATH = Environment.DIRECTORY_DCIM + "/alibi/video_recordings" val MEDIA_SUBFOLDER = "/alibi/.video_recordings"
val SCOPED_STORAGE_RELATIVE_PATH = Environment.DIRECTORY_DCIM + MEDIA_SUBFOLDER
// Parameters to be passed in descending order // Parameters to be passed in descending order
// Those parameters first try to concatenate without re-encoding // Those parameters first try to concatenate without re-encoding

View File

@ -5,7 +5,6 @@ import android.content.ContentValues
import android.content.Intent import android.content.Intent
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
import android.os.Build import android.os.Build
import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Range import android.util.Range
import androidx.camera.core.Camera import androidx.camera.core.Camera
@ -28,7 +27,7 @@ import app.myzel394.alibi.db.RecordingInformation
import app.myzel394.alibi.enums.RecorderState import app.myzel394.alibi.enums.RecorderState
import app.myzel394.alibi.helpers.BatchesFolder import app.myzel394.alibi.helpers.BatchesFolder
import app.myzel394.alibi.helpers.VideoBatchesFolder import app.myzel394.alibi.helpers.VideoBatchesFolder
import app.myzel394.alibi.ui.VIDEO_RECORDER_SUPPORTS_CUSTOM_FOLDER import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -36,6 +35,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import java.io.File
import kotlin.properties.Delegates import kotlin.properties.Delegates
class VideoRecorderService : class VideoRecorderService :
@ -227,11 +227,14 @@ class VideoRecorderService :
} }
} }
private fun getNameForMediaFile() =
"${batchesFolder.mediaPrefix}$counter.${settings.videoRecorderSettings.fileExtension}"
@SuppressLint("MissingPermission", "NewApi") @SuppressLint("MissingPermission", "NewApi")
private fun prepareVideoRecording() = private fun prepareVideoRecording() =
videoCapture!!.output videoCapture!!.output
.let { .let {
if (batchesFolder.type == BatchesFolder.BatchType.CUSTOM && VIDEO_RECORDER_SUPPORTS_CUSTOM_FOLDER) { if (batchesFolder.type == BatchesFolder.BatchType.CUSTOM && SUPPORTS_SCOPED_STORAGE) {
it.prepareRecording( it.prepareRecording(
this, this,
FileDescriptorOutputOptions.Builder( FileDescriptorOutputOptions.Builder(
@ -242,15 +245,15 @@ class VideoRecorderService :
).build() ).build()
) )
} else if (batchesFolder.type == BatchesFolder.BatchType.MEDIA) { } else if (batchesFolder.type == BatchesFolder.BatchType.MEDIA) {
if (SUPPORTS_SCOPED_STORAGE) {
it.prepareRecording( it.prepareRecording(
this, this,
MediaStoreOutputOptions.Builder( MediaStoreOutputOptions.Builder(
contentResolver, contentResolver,
batchesFolder.mediaContentUri, batchesFolder.scopedMediaContentUri,
).setContentValues( ).setContentValues(
ContentValues().apply { ContentValues().apply {
val name = val name = getNameForMediaFile()
"${batchesFolder.mediaPrefix}$counter.${settings.videoRecorderSettings.fileExtension}"
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put( put(
@ -259,7 +262,7 @@ class VideoRecorderService :
) )
put( put(
MediaStore.Video.Media.RELATIVE_PATH, MediaStore.Video.Media.RELATIVE_PATH,
VideoBatchesFolder.MEDIA_RELATIVE_PATH, VideoBatchesFolder.SCOPED_STORAGE_RELATIVE_PATH,
) )
} }
@ -270,6 +273,20 @@ class VideoRecorderService :
} }
).build() ).build()
) )
} else {
val name = getNameForMediaFile()
val file = File(
batchesFolder.legacyMediaFolder,
name
).apply {
createNewFile()
}
it.prepareRecording(
this,
FileOutputOptions.Builder(file).build()
)
}
} else { } else {
it.prepareRecording( it.prepareRecording(
this, this,

View File

@ -2,13 +2,14 @@ package app.myzel394.alibi.ui
import android.os.Build import android.os.Build
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import java.io.File
val BIG_PRIMARY_BUTTON_SIZE = 64.dp val BIG_PRIMARY_BUTTON_SIZE = 64.dp
val MAX_AMPLITUDE = 20000 val MAX_AMPLITUDE = 20000
val SUPPORTS_DARK_MODE_NATIVELY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q val SUPPORTS_DARK_MODE_NATIVELY = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
val RECORDER_SUBFOLDER_NAME = ".recordings" val RECORDER_SUBFOLDER_NAME = ".recordings"
val VIDEO_RECORDER_SUPPORTS_CUSTOM_FOLDER = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
// TODO: Fix!
val SUPPORTS_SCOPED_STORAGE = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
val MEDIA_RECORDINGS_PREFIX = "alibi-recording-" val MEDIA_RECORDINGS_PREFIX = "alibi-recording-"
val RECORDER_MEDIA_SELECTED_VALUE = "_'media" val RECORDER_MEDIA_SELECTED_VALUE = "_'media"
val RECORDER_INTERNAL_SELECTED_VALUE = "_'internal" val RECORDER_INTERNAL_SELECTED_VALUE = "_'internal"

View File

@ -35,12 +35,11 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.documentfile.provider.DocumentFile
import app.myzel394.alibi.R import app.myzel394.alibi.R
import app.myzel394.alibi.dataStore import app.myzel394.alibi.dataStore
import app.myzel394.alibi.db.AppSettings import app.myzel394.alibi.db.AppSettings
import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE import app.myzel394.alibi.ui.RECORDER_MEDIA_SELECTED_VALUE
import app.myzel394.alibi.ui.VIDEO_RECORDER_SUPPORTS_CUSTOM_FOLDER import app.myzel394.alibi.ui.SUPPORTS_SCOPED_STORAGE
import app.myzel394.alibi.ui.components.atoms.SettingsTile import app.myzel394.alibi.ui.components.atoms.SettingsTile
import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog import app.myzel394.alibi.ui.utils.rememberFolderSelectorDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -219,7 +218,7 @@ fun SaveFolderTile(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} }
if (!VIDEO_RECORDER_SUPPORTS_CUSTOM_FOLDER) { if (!SUPPORTS_SCOPED_STORAGE) {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,