From d6fd05c3a7d529cffc99928fdf45ff3dbf52d662 Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:10:05 +0700 Subject: [PATCH] refactor(io): use isContentUri instead of inline content:// checks The same startsWith check was inlined in 21 places across services and providers. They now call the existing helper, which has identical semantics (no trim, no case folding). No behavior change. --- .../download_history_provider_maintenance.dart | 6 +++--- lib/providers/local_library_provider.dart | 14 ++++++-------- lib/providers/music_player_provider.dart | 3 ++- lib/providers/playback_provider.dart | 2 +- lib/services/download_completion_audio.dart | 3 ++- lib/services/download_container_finalizer.dart | 3 ++- lib/services/download_file_finalizer.dart | 5 +++-- lib/services/m3u_playlist_service.dart | 3 ++- lib/services/music_haptics.dart | 5 +++-- lib/services/platform_bridge.dart | 6 +++--- 10 files changed, 27 insertions(+), 23 deletions(-) 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;