diff --git a/lib/providers/download_history_provider_maintenance.dart b/lib/providers/download_history_provider_maintenance.dart index 3f102e99..06001341 100644 --- a/lib/providers/download_history_provider_maintenance.dart +++ b/lib/providers/download_history_provider_maintenance.dart @@ -248,7 +248,7 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier { bool _supportsAudioMetadataProbe(String filePath) { final trimmed = filePath.trim().toLowerCase(); if (trimmed.isEmpty) return false; - if (trimmed.startsWith('content://')) return true; + if (isContentUri(trimmed)) return true; return trimmed.endsWith('.flac') || trimmed.endsWith('.m4a') || trimmed.endsWith('.mp4') || @@ -297,7 +297,7 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier { trimmedPath.endsWith('.m4a') || trimmedPath.endsWith('.mp4') || trimmedPath.endsWith('.aac') || - trimmedPath.startsWith('content://')); + isContentUri(trimmedPath)); if (hasResolvedSpecs && !isPlaceholderQualityLabel(item.quality)) { final needsComposerBackfill = @@ -382,7 +382,7 @@ extension _HistoryStartupMaintenance on DownloadHistoryNotifier { String? tempPath; try { - if (filePath.startsWith('content://')) { + if (isContentUri(filePath)) { final treeUri = normalizeOptionalString(item.downloadTreeUri); final fileName = normalizeOptionalString(item.safFileName); if (treeUri == null || fileName == null) return null; diff --git a/lib/providers/local_library_provider.dart b/lib/providers/local_library_provider.dart index 546529d2..e55e44dc 100644 --- a/lib/providers/local_library_provider.dart +++ b/lib/providers/local_library_provider.dart @@ -591,7 +591,7 @@ class LocalLibraryNotifier extends Notifier { } String _displayNameForPath(String path) { - if (path.startsWith('content://')) { + if (isContentUri(path)) { try { final decoded = Uri.decodeComponent(Uri.parse(path).pathSegments.last); final relative = decoded.contains(':') @@ -611,7 +611,7 @@ class LocalLibraryNotifier extends Notifier { } bool _looksLikeRemovablePath(String path) { - if (path.startsWith('content://')) { + if (isContentUri(path)) { try { final decoded = Uri.decodeComponent(Uri.parse(path).pathSegments.last); return !decoded.startsWith('primary:'); @@ -636,7 +636,7 @@ class LocalLibraryNotifier extends Notifier { if (_availabilityProbe != null) { return await _availabilityProbe(path, bookmark: bookmark); } - if (Platform.isAndroid && path.startsWith('content://')) { + if (Platform.isAndroid && isContentUri(path)) { return await PlatformBridge.probeSafTreeReadAccess(path); } if (Platform.isIOS && bookmark != null && bookmark.trim().isNotEmpty) { @@ -970,7 +970,7 @@ class LocalLibraryNotifier extends Notifier { if (!ref.mounted || _scanCancelRequested) return; final effectiveFolderPath = resolvedPath ?? folderPath; - final isSaf = effectiveFolderPath.startsWith('content://'); + final isSaf = isContentUri(effectiveFolderPath); final useStreamingFullScan = effectiveFolderPath.startsWith('network://') || @@ -1777,9 +1777,7 @@ class LocalLibraryNotifier extends Notifier { } if (isSaf) { - final uris = legacyPaths - .where((path) => path.startsWith('content://')) - .toList(); + final uris = legacyPaths.where((path) => isContentUri(path)).toList(); if (uris.isEmpty) { return const {}; } @@ -1805,7 +1803,7 @@ class LocalLibraryNotifier extends Notifier { } final paths = legacyPaths - .where((path) => !path.startsWith('content://')) + .where((path) => !isContentUri(path)) .toList(growable: false); const chunkSize = 24; final backfilled = {}; diff --git a/lib/providers/music_player_provider.dart b/lib/providers/music_player_provider.dart index 2ee230cf..e27246d2 100644 --- a/lib/providers/music_player_provider.dart +++ b/lib/providers/music_player_provider.dart @@ -5,6 +5,7 @@ import 'package:spotiflac_android/models/unified_library_item.dart'; import 'package:spotiflac_android/providers/download_queue_provider.dart'; import 'package:spotiflac_android/services/library_database.dart'; import 'package:spotiflac_android/services/music_player_service.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; final musicPlayerRuntimeProvider = Provider( (ref) => musicPlayerRuntime, @@ -301,7 +302,7 @@ PlayableMedia playableFromLocal(LocalLibraryItem item) { String? art; final cover = item.coverPath; if (cover != null && cover.trim().isNotEmpty) { - art = cover.startsWith('http') || cover.startsWith('content://') + art = cover.startsWith('http') || isContentUri(cover) ? cover : Uri.file(cover).toString(); } diff --git a/lib/providers/playback_provider.dart b/lib/providers/playback_provider.dart index 16726a3d..c6fd8dd0 100644 --- a/lib/providers/playback_provider.dart +++ b/lib/providers/playback_provider.dart @@ -40,7 +40,7 @@ class PlaybackController extends Notifier { final value = cover.trim(); if (value.isEmpty) return null; if (value.startsWith('http') || - value.startsWith('content://') || + isContentUri(value) || value.startsWith('file://')) { return value; } diff --git a/lib/services/download_completion_audio.dart b/lib/services/download_completion_audio.dart index 4eb72913..d113f75d 100644 --- a/lib/services/download_completion_audio.dart +++ b/lib/services/download_completion_audio.dart @@ -1,5 +1,6 @@ import 'package:spotiflac_android/models/download_result.dart'; import 'package:spotiflac_android/utils/audio_format_utils.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/int_utils.dart'; typedef DownloadCompletionAudio = ({ @@ -30,7 +31,7 @@ Future resolveDownloadCompletionAudio({ normalizeAudioFormatValue(audioFormatForPath(filePath)); var bitrate = download.bitrateKbps; final lowerPath = filePath.toLowerCase(); - if (filePath.startsWith('content://') || + if (isContentUri(filePath) || const [ '.flac', '.m4a', diff --git a/lib/services/download_container_finalizer.dart b/lib/services/download_container_finalizer.dart index cda2db5b..7a3d920a 100644 --- a/lib/services/download_container_finalizer.dart +++ b/lib/services/download_container_finalizer.dart @@ -1,6 +1,7 @@ import 'package:spotiflac_android/models/download_result.dart'; import 'package:spotiflac_android/services/download_saf_file_replacer.dart'; import 'package:spotiflac_android/utils/audio_format_utils.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; /// Finalizes a requested lossless container using the measured audio stream. /// Native FLAC bytes can be renamed; FLAC-in-MP4 still needs remuxing. @@ -36,7 +37,7 @@ class DownloadContainerFinalizer { forceConversion = forceConversion || result.requiresContainerConversion; final outputExtension = result.outputExtension(path: filePath); final lowerPath = filePath.toLowerCase(); - final isSafSource = filePath.startsWith('content://'); + final isSafSource = isContentUri(filePath); final pathLooksLikeM4a = lowerPath.endsWith('.m4a') || lowerPath.endsWith('.mp4') || diff --git a/lib/services/download_file_finalizer.dart b/lib/services/download_file_finalizer.dart index 825896c0..d9c8a89b 100644 --- a/lib/services/download_file_finalizer.dart +++ b/lib/services/download_file_finalizer.dart @@ -5,6 +5,7 @@ import 'package:spotiflac_android/services/download_saf_file_replacer.dart'; import 'package:spotiflac_android/services/ffmpeg_models.dart'; import 'package:spotiflac_android/utils/audio_conversion_utils.dart'; import 'package:spotiflac_android/utils/audio_format_utils.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; typedef DecryptDownloadFile = Future Function({ @@ -120,7 +121,7 @@ class DownloadFileFinalizer { return DownloadDecryptionOutcome(filePath); } onStart?.call(descriptor.normalizedStrategy); - if (useSaf && filePath.startsWith('content://')) { + if (useSaf && isContentUri(filePath)) { if (treeUri == null || treeUri.isEmpty) { return const DownloadDecryptionOutcome( null, @@ -239,7 +240,7 @@ class DownloadFileFinalizer { onStart?.call(); String? convertedPath; String? publishedName = convertedName; - if (useSaf && filePath.startsWith('content://')) { + if (useSaf && isContentUri(filePath)) { if (treeUri == null || treeUri.isEmpty) { throw StateError('Missing SAF tree for automatic conversion'); } diff --git a/lib/services/m3u_playlist_service.dart b/lib/services/m3u_playlist_service.dart index edf1d32f..873986bf 100644 --- a/lib/services/m3u_playlist_service.dart +++ b/lib/services/m3u_playlist_service.dart @@ -6,6 +6,7 @@ import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('M3uPlaylist'); @@ -122,7 +123,7 @@ class M3uPlaylistService { static String? exportPathFor(String rawPath) { final trimmed = rawPath.trim(); if (trimmed.isEmpty) return null; - if (!trimmed.startsWith('content://')) return trimmed; + if (!isContentUri(trimmed)) return trimmed; try { // Uri.pathSegments already percent-decodes each segment once. diff --git a/lib/services/music_haptics.dart b/lib/services/music_haptics.dart index 4c926ab4..54e0b32a 100644 --- a/lib/services/music_haptics.dart +++ b/lib/services/music_haptics.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:path_provider/path_provider.dart'; import 'package:spotiflac_android/services/audio_analysis_jobs.dart'; import 'package:spotiflac_android/services/platform_bridge.dart'; +import 'package:spotiflac_android/utils/file_access.dart'; import 'package:spotiflac_android/utils/logger.dart' show AppLogger; const musicHapticsSampleRate = 4000; @@ -177,7 +178,7 @@ class MusicHapticsAnalyzer { List arguments, ) async { final inputIndex = arguments.indexOf('-i') + 1; - if (inputIndex > 0 && arguments[inputIndex].startsWith('content://')) { + if (inputIndex > 0 && isContentUri(arguments[inputIndex])) { // Resolve only when this owned job starts. FFmpegKit opens and closes // its own SAF descriptor, independently of the player's short lease. final input = await FFmpegKitConfig.getSafParameterForRead( @@ -221,7 +222,7 @@ class MusicHapticsAnalyzer { Directory? work; try { String? key; - if (path.startsWith('content://')) { + if (isContentUri(path)) { // A document URI is not a filesystem path. Unknown modification times // still allow analysis, but cannot safely identify a cached revision. try { diff --git a/lib/services/platform_bridge.dart b/lib/services/platform_bridge.dart index 1791a000..b595b9e1 100644 --- a/lib/services/platform_bridge.dart +++ b/lib/services/platform_bridge.dart @@ -1008,7 +1008,7 @@ class PlatformBridge { static Future openContentUriPlaybackLease( String uri, ) async { - if (!Platform.isAndroid || !uri.startsWith('content://')) return null; + if (!Platform.isAndroid || !isContentUri(uri)) return null; final result = await _channel.invokeMethod('safOpenPlaybackLease', { 'uri': uri, }); @@ -2688,7 +2688,7 @@ class PlatformBridge { }) async { final input = {}; if (itemId != null && itemId.isNotEmpty) input['item_id'] = itemId; - if (filePath.startsWith('content://')) { + if (isContentUri(filePath)) { input['uri'] = filePath; } else { input['path'] = filePath; @@ -2704,7 +2704,7 @@ class PlatformBridge { final uri = result['new_file_uri']; final path = result['new_file_path']; final legacyPath = result['file_path']; - if (filePath.startsWith('content://') && uri is String && uri.isNotEmpty) { + if (isContentUri(filePath) && uri is String && uri.isNotEmpty) { result['file_path'] = uri; } else if (path is String && path.isNotEmpty) { result['file_path'] = path;