Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4078,13 +4078,20 @@ class PlayerViewModel @Inject constructor(
}
if (deleteRequests.size == deletableSongs.size) {
val uris = deleteRequests.map { it.second }.distinctBy { it.toString() }
val intentSender = com.theveloper.pixelplay.utils.MediaStorePermissionHelper
.createDeleteRequestIntentSender(activity, uris)
if (intentSender != null) {
pendingBatchDeleteSongs = deleteRequests.map { it.first }
pendingBatchDeleteSkippedCount = skippedCount
val deleteRequest = com.theveloper.pixelplay.utils.MediaStorePermissionHelper
.createDeleteRequest(activity, uris)
if (deleteRequest != null) {
val acceptedUriStrings = deleteRequest.acceptedUris
.mapTo(mutableSetOf()) { it.toString() }
val acceptedSongs = deleteRequests
.filter { (_, uri) -> uri.toString() in acceptedUriStrings }
.map { it.first }
val invalidRequestCount = deletableSongs.size - acceptedSongs.size

pendingBatchDeleteSongs = acceptedSongs
pendingBatchDeleteSkippedCount = skippedCount + invalidRequestCount
pendingBatchDeleteOnComplete = onComplete
_deletePermissionRequest.emit(intentSender)
_deletePermissionRequest.emit(deleteRequest.intentSender)
return@launch
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,12 @@ import androidx.annotation.RequiresApi
object MediaStorePermissionHelper {
private const val MEDIASTORE_AUTHORITY = "media"

data class DeleteRequest(
val intentSender: IntentSender,
val acceptedUris: List<Uri>,
val rejectedUris: List<Uri>
)

/**
* Returns the MediaStore content URI for a given audio song ID.
* Returns null for cloud songs (negative IDs).
Expand All @@ -32,6 +38,27 @@ object MediaStorePermissionHelper {
}
}

fun getAudioMediaStoreUris(context: Context, songId: Long): List<Uri> {
if (songId <= 0) return emptyList()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return listOf(ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, songId))
}

val specificVolumes = runCatching {
MediaStore.getExternalVolumeNames(context)
}.getOrDefault(emptySet())
.filterNot { it == MediaStore.VOLUME_EXTERNAL }
.sortedWith(compareBy<String> { it != MediaStore.VOLUME_EXTERNAL_PRIMARY }.thenBy { it })

return buildList {
specificVolumes.forEach { volumeName ->
add(MediaStore.Audio.Media.getContentUri(volumeName, songId))
}
add(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY, songId))
add(MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL, songId))
}.distinctBy { it.toString() }
}

fun isMediaStoreItemUriString(contentUriString: String): Boolean {
val normalized = contentUriString.trim().lowercase()
if (!normalized.startsWith("content://$MEDIASTORE_AUTHORITY/")) return false
Expand All @@ -52,14 +79,19 @@ object MediaStorePermissionHelper {
contentUriString: String,
filePath: String
): Uri? {
val parsedMediaStoreUri = parseMediaStoreItemUri(contentUriString)
val parsedSongId = parsedMediaStoreUri?.lastPathSegment?.toLongOrNull()
val candidates = buildList {
parseMediaStoreItemUri(contentUriString)?.let(::add)
if (songId != null && canUseSongIdForMediaStoreRequest(contentUriString)) {
addAll(getAudioMediaStoreUris(context, songId))
}
if (parsedSongId != null) {
addAll(getAudioMediaStoreUris(context, parsedSongId))
}
parsedMediaStoreUri?.let(::add)
if (filePath.isNotBlank()) {
getMediaStoreUri(context, filePath)?.let(::add)
}
if (songId != null && canUseSongIdForMediaStoreRequest(contentUriString)) {
getMediaStoreUri(songId)?.let(::add)
}
}.distinctBy { it.toString() }

return candidates.firstOrNull { uri ->
Expand Down Expand Up @@ -122,12 +154,47 @@ object MediaStorePermissionHelper {
context: Context,
uris: Collection<Uri>
): IntentSender? {
if (uris.isEmpty()) return null
return createDeleteRequest(context, uris)?.intentSender
}

@RequiresApi(Build.VERSION_CODES.R)
fun createDeleteRequest(
context: Context,
uris: Collection<Uri>
): DeleteRequest? {
val distinctUris = uris.distinctBy { it.toString() }
if (distinctUris.isEmpty()) return null

createPlatformDeleteRequest(context, distinctUris)?.let { intentSender ->
return DeleteRequest(
intentSender = intentSender,
acceptedUris = distinctUris,
rejectedUris = emptyList()
)
}

val acceptedUris = distinctUris.filter { uri ->
createPlatformDeleteRequest(context, listOf(uri)) != null
}
if (acceptedUris.isEmpty()) return null

val intentSender = createPlatformDeleteRequest(context, acceptedUris) ?: return null
val acceptedUriStrings = acceptedUris.mapTo(mutableSetOf()) { it.toString() }
return DeleteRequest(
intentSender = intentSender,
acceptedUris = acceptedUris,
rejectedUris = distinctUris.filterNot { it.toString() in acceptedUriStrings }
)
}

@RequiresApi(Build.VERSION_CODES.R)
private fun createPlatformDeleteRequest(
context: Context,
uris: Collection<Uri>
): IntentSender? {
return try {
MediaStore.createDeleteRequest(context.contentResolver, uris).intentSender
} catch (_: IllegalArgumentException) {
null
} catch (_: SecurityException) {
} catch (_: Exception) {
null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ class MediaStorePermissionHelperTest {
"content://media/external/audio/media/9317"
)
).isTrue()
assertThat(
MediaStorePermissionHelper.isMediaStoreItemUriString(
"content://media/external_primary/audio/media/9317"
)
).isTrue()
}

@Test
Expand All @@ -21,6 +26,11 @@ class MediaStorePermissionHelperTest {
"content://media/external/audio/media"
)
).isFalse()
assertThat(
MediaStorePermissionHelper.isMediaStoreItemUriString(
"content://media/external/audio/media/not-a-number"
)
).isFalse()
}

@Test
Expand Down
Loading