mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-27 21:30:23 +02:00
fix(ios): stop dropping the security-scoped download folder bookmark
Fixes #439, likely also #302. Reported: on iOS, the download folder silently reverts to the default SpotiFLAC folder after starting a download (and stays reverted after closing and reopening the app). _processQueue ran an iCloud/writable-path shape check against the persisted downloadDirectory string and, if it looked like iCloud Drive or failed the structural writable-path check, called setDownloadDirectory() to reset it to the default folder. That call passes no iosBookmark argument, so it also wipes the user's security-scoped bookmark in the persisted settings - permanently, not just for the current run. This check ran unconditionally, even when a bookmark was already present for that folder. A folder picked from Files can legitimately have a persisted path that looks like iCloud Drive, or that fails this generic structural check, while the bookmark itself still grants real write access - the bookmark is the actual source of truth, and the queue already has a separate, correct bookmark-resolution step right after this one (StartAccessingIosBookmark, with its own proper failure handling that fails the queued items with a clear message instead of destroying the setting). The earlier path-shape check just never deferred to it. lib/utils/file_access.dart: extract the run/skip decision into shouldValidateIosOutputDir(isIOS, isSafMode, outputDir, downloadDirectoryBookmark) - skips whenever a bookmark is present - and use it in download_queue_provider.dart in place of the inline condition. Kept as a plain function taking isIOS as a parameter since Platform.isIOS itself can't be exercised from a host test run. test/file_access_ios_test.dart covers: runs for a plain app-folder path, skips when a bookmark is present (the exact #439 case), skips off iOS, skips in SAF mode, and skips when there's no output dir yet. Verification: flutter analyze and flutter test (224 tests, all green, including the 5 new ones) both clean.
This commit is contained in:
@@ -1470,8 +1470,15 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
|||||||
await _initOutputDir();
|
await _initOutputDir();
|
||||||
}
|
}
|
||||||
|
|
||||||
// iOS: Validate that outputDir is writable (not iCloud Drive which Go can't access)
|
// iOS: Validate that outputDir is writable (not iCloud Drive which Go
|
||||||
if (!isSafMode && Platform.isIOS && state.outputDir.isNotEmpty) {
|
// can't access), unless a bookmark makes this app-Documents path-shape
|
||||||
|
// check irrelevant (see shouldValidateIosOutputDir).
|
||||||
|
if (shouldValidateIosOutputDir(
|
||||||
|
isIOS: Platform.isIOS,
|
||||||
|
isSafMode: isSafMode,
|
||||||
|
outputDir: state.outputDir,
|
||||||
|
downloadDirectoryBookmark: settings.downloadDirectoryBookmark,
|
||||||
|
)) {
|
||||||
final isICloudPath =
|
final isICloudPath =
|
||||||
state.outputDir.contains('Mobile Documents') ||
|
state.outputDir.contains('Mobile Documents') ||
|
||||||
state.outputDir.contains('CloudDocs') ||
|
state.outputDir.contains('CloudDocs') ||
|
||||||
|
|||||||
@@ -6,6 +6,30 @@ import 'package:spotiflac_android/services/music_player_service.dart';
|
|||||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||||
import 'package:spotiflac_android/utils/mime_utils.dart';
|
import 'package:spotiflac_android/utils/mime_utils.dart';
|
||||||
|
|
||||||
|
/// Whether the queue should run its iOS output-directory path-shape check
|
||||||
|
/// (which can replace [outputDir] with the default Documents folder when the
|
||||||
|
/// path looks like iCloud Drive or an invalid container path).
|
||||||
|
///
|
||||||
|
/// A security-scoped bookmark is the source of truth for a folder picked
|
||||||
|
/// from Files: its persisted path may legitimately contain substrings that
|
||||||
|
/// look like iCloud Drive, or fail the structural writable-path check, even
|
||||||
|
/// though the bookmark itself grants real write access. Running the
|
||||||
|
/// path-shape check anyway would reset - and via setDownloadDirectory,
|
||||||
|
/// permanently drop - a perfectly valid bookmarked folder. When a bookmark
|
||||||
|
/// is present, the queue's later bookmark-resolution step is authoritative
|
||||||
|
/// instead.
|
||||||
|
bool shouldValidateIosOutputDir({
|
||||||
|
required bool isIOS,
|
||||||
|
required bool isSafMode,
|
||||||
|
required String outputDir,
|
||||||
|
required String downloadDirectoryBookmark,
|
||||||
|
}) {
|
||||||
|
return isIOS &&
|
||||||
|
!isSafMode &&
|
||||||
|
outputDir.isNotEmpty &&
|
||||||
|
downloadDirectoryBookmark.isEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
/// Regular expression to detect iOS app container paths.
|
/// Regular expression to detect iOS app container paths.
|
||||||
/// Matches paths like /var/mobile/Containers/Data/Application/{UUID}
|
/// Matches paths like /var/mobile/Containers/Data/Application/{UUID}
|
||||||
/// or /private/var/mobile/Containers/Data/Application/{UUID}
|
/// or /private/var/mobile/Containers/Data/Application/{UUID}
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:spotiflac_android/utils/file_access.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('shouldValidateIosOutputDir', () {
|
||||||
|
test('validates a non-empty app-folder path on iOS', () {
|
||||||
|
expect(
|
||||||
|
shouldValidateIosOutputDir(
|
||||||
|
isIOS: true,
|
||||||
|
isSafMode: false,
|
||||||
|
outputDir:
|
||||||
|
'/private/var/mobile/Containers/Data/Application/ABC/Documents/SpotiFLAC',
|
||||||
|
downloadDirectoryBookmark: '',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'skips validation when a security-scoped bookmark is present (#439)',
|
||||||
|
() {
|
||||||
|
// The persisted path may look like iCloud Drive or fail the
|
||||||
|
// structural writable-path check even though the bookmark grants
|
||||||
|
// real write access - the bookmark, not this path shape, is
|
||||||
|
// authoritative. Running the check anyway is what silently reset
|
||||||
|
// and dropped the user's chosen folder.
|
||||||
|
expect(
|
||||||
|
shouldValidateIosOutputDir(
|
||||||
|
isIOS: true,
|
||||||
|
isSafMode: false,
|
||||||
|
outputDir:
|
||||||
|
'/private/var/mobile/Library/Mobile Documents/com~apple~CloudDocs/Music',
|
||||||
|
downloadDirectoryBookmark: 'bookmark-bytes',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('skips validation off iOS', () {
|
||||||
|
expect(
|
||||||
|
shouldValidateIosOutputDir(
|
||||||
|
isIOS: false,
|
||||||
|
isSafMode: false,
|
||||||
|
outputDir: '/data/user/0/com.zarz.spotiflac/files/SpotiFLAC',
|
||||||
|
downloadDirectoryBookmark: '',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips validation in SAF mode', () {
|
||||||
|
expect(
|
||||||
|
shouldValidateIosOutputDir(
|
||||||
|
isIOS: true,
|
||||||
|
isSafMode: true,
|
||||||
|
outputDir: '/some/path',
|
||||||
|
downloadDirectoryBookmark: '',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('skips validation when there is no output directory yet', () {
|
||||||
|
expect(
|
||||||
|
shouldValidateIosOutputDir(
|
||||||
|
isIOS: true,
|
||||||
|
isSafMode: false,
|
||||||
|
outputDir: '',
|
||||||
|
downloadDirectoryBookmark: '',
|
||||||
|
),
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user