diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt index 283fb65a1..778297d58 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongInfoBottomSheet.kt @@ -437,21 +437,24 @@ fun SongInfoBottomSheet( text = song.title ) } - FilledTonalIconButton( - modifier = Modifier - .fillMaxHeight() - .padding(vertical = 6.dp), - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceBright, - contentColor = MaterialTheme.colorScheme.onSurface - ), - onClick = { showEditSheet = true }, - ) { - Icon( - modifier = Modifier.padding(horizontal = 8.dp), - imageVector = Icons.Rounded.Edit, - contentDescription = stringResource(R.string.cd_edit_song_metadata) - ) + val isEditable = remember(song) { songInfoViewModel.isSongEditable(song) } + if (isEditable) { + FilledTonalIconButton( + modifier = Modifier + .fillMaxHeight() + .padding(vertical = 6.dp), + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceBright, + contentColor = MaterialTheme.colorScheme.onSurface + ), + onClick = { showEditSheet = true }, + ) { + Icon( + modifier = Modifier.padding(horizontal = 8.dp), + imageVector = Icons.Rounded.Edit, + contentDescription = stringResource(R.string.cd_edit_song_metadata) + ) + } } } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongInfoBottomSheetViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongInfoBottomSheetViewModel.kt index 723a39e07..2e94f6957 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongInfoBottomSheetViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/SongInfoBottomSheetViewModel.kt @@ -254,13 +254,27 @@ class SongInfoBottomSheetViewModel @Inject constructor( return transferStateStore.isSongSavedOnAllReachableWatches(songId) } + fun isSongEditable(song: Song): Boolean { + if (getCloudProviderLabel(song.contentUriString) != null) return false + + if (song.path.isNotBlank()) { + val file = File(song.path) + return file.exists() && file.isFile + } + + val uri = song.contentUriString + return uri.startsWith("content://") || uri.startsWith("file://") + } + private fun getCloudProviderLabel(contentUriString: String): String? { + val normalized = contentUriString.lowercase().trim() return when { - contentUriString.startsWith("telegram://") -> "Telegram" - contentUriString.startsWith("netease://") -> "Netease Music" - contentUriString.startsWith("qqmusic://") -> "QQ Music" - contentUriString.startsWith("navidrome://") -> "Navidrome" - contentUriString.startsWith("gdrive://") -> "Google Drive" + normalized.startsWith("telegram://") || normalized.startsWith("telegram:") -> "Telegram" + normalized.startsWith("netease://") || normalized.startsWith("netease:") -> "Netease Music" + normalized.startsWith("qqmusic://") || normalized.startsWith("qqmusic:") -> "QQ Music" + normalized.startsWith("navidrome://") || normalized.startsWith("navidrome:") -> "Navidrome" + normalized.startsWith("gdrive://") || normalized.startsWith("gdrive:") -> "Google Drive" + normalized.startsWith("jellyfin://") || normalized.startsWith("jellyfin:") -> "Jellyfin" else -> null } } diff --git a/app/src/main/java/com/theveloper/pixelplay/utils/MediaStorePermissionHelper.kt b/app/src/main/java/com/theveloper/pixelplay/utils/MediaStorePermissionHelper.kt index 5e99be993..cfa6f6f54 100644 --- a/app/src/main/java/com/theveloper/pixelplay/utils/MediaStorePermissionHelper.kt +++ b/app/src/main/java/com/theveloper/pixelplay/utils/MediaStorePermissionHelper.kt @@ -201,7 +201,49 @@ object MediaStorePermissionHelper { uris: Collection ): IntentSender? { if (uris.isEmpty()) return null - return MediaStore.createWriteRequest(context.contentResolver, uris).intentSender + + // Filter out URIs that do not exist in the MediaStore database + // to avoid IllegalArgumentException: Invalid Uri + val existingIds = try { + val projection = arrayOf(MediaStore.Files.FileColumns._ID) + val idList = uris.mapNotNull { it.lastPathSegment?.toLongOrNull() } + if (idList.isEmpty()) { + emptySet() + } else { + val selection = "${MediaStore.Files.FileColumns._ID} IN (${idList.joinToString(",") { "?" }})" + val selectionArgs = idList.map { it.toString() }.toTypedArray() + context.contentResolver.query( + MediaStore.Files.getContentUri("external"), + projection, + selection, + selectionArgs, + null + )?.use { cursor -> + val idSet = mutableSetOf() + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID) + while (cursor.moveToNext()) { + idSet.add(cursor.getLong(idColumn)) + } + idSet + } ?: emptySet() + } + } catch (e: Exception) { + emptySet() + } + + val validUris = uris.filter { uri -> + val id = uri.lastPathSegment?.toLongOrNull() + id != null && id in existingIds + } + + if (validUris.isEmpty()) return null + + return try { + MediaStore.createWriteRequest(context.contentResolver, validUris).intentSender + } catch (e: Exception) { + android.util.Log.e("MediaStorePermissionHelper", "Failed to create write request for URIs: $validUris", e) + null + } } /**