fix: permission error message and Android 13+ storage permission

- Fixed download showing 'Song not found' when actually permission error
- Added permission error type detection in Go backend
- Android 13+ now requests both MANAGE_EXTERNAL_STORAGE and READ_MEDIA_AUDIO
- MANAGE_EXTERNAL_STORAGE opens Settings (system-level)
- READ_MEDIA_AUDIO shows dialog (app-level, resets on clear data)
- Proper permission check before showing 'granted' status
This commit is contained in:
zarzet
2026-01-12 19:56:12 +07:00
parent d417743654
commit d98960d053
6 changed files with 104 additions and 19 deletions
+8
View File
@@ -8,6 +8,14 @@
- Detects existing entries by Spotify ID, Deezer ID, or ISRC
- Replaces existing entry and moves to top of list
- Auto-deduplicates existing history on app load
- **Permission Error Message**: Fixed download showing "Song not found" when actually a permission error
- Now shows proper message: "Cannot write to folder, check storage permission"
- Added `permission` error type detection in backend
- **Android 13+ Storage Permission**: Fixed storage permission not working on Android 13+
- Android 13+ now requests both `MANAGE_EXTERNAL_STORAGE` and `READ_MEDIA_AUDIO`
- `MANAGE_EXTERNAL_STORAGE` opens Settings (system-level, persists across app data clear)
- `READ_MEDIA_AUDIO` shows dialog (app-level, resets on app data clear)
- Proper permission check before showing "granted" status
---
+6
View File
@@ -991,6 +991,12 @@ func errorResponse(msg string) (string, error) {
strings.Contains(lowerMsg, "try using vpn") ||
strings.Contains(lowerMsg, "change dns") {
errorType = "isp_blocked"
} else if strings.Contains(lowerMsg, "permission") ||
strings.Contains(lowerMsg, "operation not permitted") ||
strings.Contains(lowerMsg, "access denied") ||
strings.Contains(lowerMsg, "failed to create file") ||
strings.Contains(lowerMsg, "failed to create directory") {
errorType = "permission"
} else if strings.Contains(lowerMsg, "not found") ||
strings.Contains(lowerMsg, "not available") ||
strings.Contains(lowerMsg, "no results") ||
+3
View File
@@ -19,6 +19,7 @@ enum DownloadErrorType {
notFound, // Track not found on any service
rateLimit, // Rate limited by service
network, // Network/connection error
permission, // File/folder permission error
}
@JsonSerializable()
@@ -88,6 +89,8 @@ class DownloadItem {
return 'Rate limit reached, try again later';
case DownloadErrorType.network:
return 'Connection failed, check your internet';
case DownloadErrorType.permission:
return 'Cannot write to folder, check storage permission';
default:
return error ?? 'An error occurred';
}
+1
View File
@@ -51,4 +51,5 @@ const _$DownloadErrorTypeEnumMap = {
DownloadErrorType.notFound: 'notFound',
DownloadErrorType.rateLimit: 'rateLimit',
DownloadErrorType.network: 'network',
DownloadErrorType.permission: 'permission',
};
@@ -1677,6 +1677,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
case 'network':
errorType = DownloadErrorType.network;
break;
case 'permission':
errorType = DownloadErrorType.permission;
break;
default:
errorType = DownloadErrorType.unknown;
}
+83 -19
View File
@@ -66,24 +66,38 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
}
} else if (Platform.isAndroid) {
// Check storage permission
PermissionStatus storageStatus;
bool storageGranted = false;
if (_androidSdkVersion >= 33) {
storageStatus = await Permission.audio.status;
// Android 13+: Need BOTH MANAGE_EXTERNAL_STORAGE AND READ_MEDIA_AUDIO
final manageStatus = await Permission.manageExternalStorage.status;
final audioStatus = await Permission.audio.status;
debugPrint('[Permission] Android 13+ check: MANAGE_EXTERNAL_STORAGE=$manageStatus, READ_MEDIA_AUDIO=$audioStatus');
storageGranted = manageStatus.isGranted && audioStatus.isGranted;
} else if (_androidSdkVersion >= 30) {
storageStatus = await Permission.manageExternalStorage.status;
// Android 11-12: Need MANAGE_EXTERNAL_STORAGE only
final manageStatus = await Permission.manageExternalStorage.status;
debugPrint('[Permission] Android 11-12 check: MANAGE_EXTERNAL_STORAGE=$manageStatus');
storageGranted = manageStatus.isGranted;
} else {
storageStatus = await Permission.storage.status;
// Android 10 and below: Use legacy storage permission
final storageStatus = await Permission.storage.status;
debugPrint('[Permission] Android 10- check: STORAGE=$storageStatus');
storageGranted = storageStatus.isGranted;
}
debugPrint('[Permission] Final storageGranted=$storageGranted');
// Check notification permission (Android 13+)
PermissionStatus notificationStatus = PermissionStatus.granted;
if (_androidSdkVersion >= 33) {
notificationStatus = await Permission.notification.status;
debugPrint('[Permission] Notification=$notificationStatus');
}
if (mounted) {
setState(() {
_storagePermissionGranted = storageStatus.isGranted;
_storagePermissionGranted = storageGranted;
_notificationPermissionGranted = notificationStatus.isGranted;
});
}
@@ -97,17 +111,57 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
if (Platform.isIOS) {
setState(() => _storagePermissionGranted = true);
} else if (Platform.isAndroid) {
PermissionStatus status;
bool allGranted = false;
if (_androidSdkVersion >= 33) {
// Android 13+: Use audio permission
status = await Permission.audio.request();
// Android 13+: Need BOTH MANAGE_EXTERNAL_STORAGE AND READ_MEDIA_AUDIO
// First check/request MANAGE_EXTERNAL_STORAGE
var manageStatus = await Permission.manageExternalStorage.status;
if (!manageStatus.isGranted) {
if (mounted) {
final shouldOpen = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Storage Access Required'),
content: const Text(
'SpotiFLAC needs "All files access" permission to save music files to your chosen folder.\n\n'
'Please enable "Allow access to manage all files" in the next screen.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Open Settings'),
),
],
),
);
if (shouldOpen == true) {
await Permission.manageExternalStorage.request();
// Re-check after returning from settings
await Future.delayed(const Duration(milliseconds: 500));
manageStatus = await Permission.manageExternalStorage.status;
}
}
}
// Then request READ_MEDIA_AUDIO (this shows a dialog)
var audioStatus = await Permission.audio.status;
if (!audioStatus.isGranted && manageStatus.isGranted) {
audioStatus = await Permission.audio.request();
}
allGranted = manageStatus.isGranted && audioStatus.isGranted;
} else if (_androidSdkVersion >= 30) {
// Android 11-12: Need MANAGE_EXTERNAL_STORAGE
// This opens system settings, not a dialog
status = await Permission.manageExternalStorage.status;
if (!status.isGranted) {
// Show explanation dialog first
// Android 11-12: Need MANAGE_EXTERNAL_STORAGE only
var manageStatus = await Permission.manageExternalStorage.status;
if (!manageStatus.isGranted) {
if (mounted) {
final shouldOpen = await showDialog<bool>(
context: context,
@@ -131,23 +185,33 @@ class _SetupScreenState extends ConsumerState<SetupScreen> {
);
if (shouldOpen == true) {
status = await Permission.manageExternalStorage.request();
await Permission.manageExternalStorage.request();
// Re-check after returning from settings
await Future.delayed(const Duration(milliseconds: 500));
manageStatus = await Permission.manageExternalStorage.status;
}
}
}
allGranted = manageStatus.isGranted;
} else {
// Android 10 and below: Use legacy storage permission
status = await Permission.storage.request();
final status = await Permission.storage.request();
allGranted = status.isGranted;
if (status.isPermanentlyDenied) {
_showPermissionDeniedDialog('Storage');
setState(() => _isLoading = false);
return;
}
}
if (status.isGranted) {
if (allGranted) {
setState(() => _storagePermissionGranted = true);
} else if (status.isPermanentlyDenied) {
_showPermissionDeniedDialog('Storage');
} else {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Permission denied. Please grant permission to continue.')),
const SnackBar(content: Text('Permission denied. Please grant all permissions to continue.')),
);
}
}