diff --git a/lib/model/source/album.dart b/lib/model/source/album.dart index e11ad2012..55f43af2f 100644 --- a/lib/model/source/album.dart +++ b/lib/model/source/album.dart @@ -29,7 +29,8 @@ mixin AlbumMixin on SourceBase { void _notifyAlbumChange() => eventBus.fire(AlbumsChangedEvent()); String getAlbumDisplayName(BuildContext? context, String dirPath) { - assert(!dirPath.endsWith(pContext.separator)); + final separator = pContext.separator; + assert(!dirPath.endsWith(separator)); if (context != null) { final type = androidFileUtils.getAlbumType(dirPath); @@ -52,8 +53,9 @@ mixin AlbumMixin on SourceBase { String unique(String dirPath, Set others) { final parts = pContext.split(dirPath); for (var i = parts.length - 1; i > 0; i--) { - final testName = pContext.joinAll(['', ...parts.skip(i)]); - if (others.every((item) => !item!.endsWith(testName))) return testName; + final name = pContext.joinAll(['', ...parts.skip(i)]); + final testName = '$separator$name'; + if (others.every((item) => !item!.endsWith(testName))) return name; } return dirPath; } diff --git a/lib/utils/android_file_utils.dart b/lib/utils/android_file_utils.dart index e071beab5..7ae61645c 100644 --- a/lib/utils/android_file_utils.dart +++ b/lib/utils/android_file_utils.dart @@ -10,7 +10,7 @@ import 'package:flutter/widgets.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { - late String primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath, videoCapturesPath; + late final String separator, primaryStorage, dcimPath, downloadPath, moviesPath, picturesPath, videoCapturesPath; Set storageVolumes = {}; Set _packages = {}; List _potentialAppDirs = []; @@ -22,9 +22,10 @@ class AndroidFileUtils { AndroidFileUtils._private(); Future init() async { + separator = pContext.separator; storageVolumes = await storageService.getStorageVolumes(); - // path_provider getExternalStorageDirectory() gives '/storage/emulated/0/Android/data/deckers.thibault.aves/files' - primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? '/'; + primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? separator; + // standard dcimPath = pContext.join(primaryStorage, 'DCIM'); downloadPath = pContext.join(primaryStorage, 'Download'); moviesPath = pContext.join(primaryStorage, 'Movies'); @@ -39,11 +40,11 @@ class AndroidFileUtils { appNameChangeNotifier.notifyListeners(); } - bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('Camera') || path.endsWith('100ANDRO')); + bool isCameraPath(String path) => path.startsWith(dcimPath) && (path.endsWith('${separator}Camera') || path.endsWith('${separator}100ANDRO')); - bool isScreenshotsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('Screenshots'); + bool isScreenshotsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(picturesPath)) && path.endsWith('${separator}Screenshots'); - bool isScreenRecordingsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(moviesPath)) && (path.endsWith('Screen recordings') || path.endsWith('ScreenRecords')); + bool isScreenRecordingsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(moviesPath)) && (path.endsWith('${separator}Screen recordings') || path.endsWith('${separator}ScreenRecords')); bool isVideoCapturesPath(String path) => path == videoCapturesPath; @@ -54,7 +55,7 @@ class AndroidFileUtils { final volume = storageVolumes.firstWhereOrNull((v) => path.startsWith(v.path)); // storage volume path includes trailing '/', but argument path may or may not, // which is an issue when the path is at the root - return volume != null || path.endsWith('/') ? volume : getStorageVolume('$path/'); + return volume != null || path.endsWith(separator) ? volume : getStorageVolume('$path$separator'); } bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; diff --git a/test/model/collection_source_test.dart b/test/model/collection_source_test.dart index e2b6bfb8a..9b7079466 100644 --- a/test/model/collection_source_test.dart +++ b/test/model/collection_source_test.dart @@ -259,6 +259,8 @@ void main() { FakeMediaStoreService.newImage('${FakeStorageService.primaryPath}Seneca', '1'), FakeMediaStoreService.newImage('${FakeStorageService.removablePath}Pictures/Cicero', '1'), FakeMediaStoreService.newImage('${FakeStorageService.removablePath}Marcus Aurelius', '1'), + FakeMediaStoreService.newImage('${FakeStorageService.primaryPath}Pictures/Hannah Arendt', '1'), + FakeMediaStoreService.newImage('${FakeStorageService.primaryPath}Pictures/Arendt', '1'), }; await androidFileUtils.init(); @@ -276,6 +278,8 @@ void main() { expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Seneca'), 'Seneca'); expect(source.getAlbumDisplayName(context, '${FakeStorageService.removablePath}Pictures/Cicero'), 'Cicero'); expect(source.getAlbumDisplayName(context, '${FakeStorageService.removablePath}Marcus Aurelius'), 'Marcus Aurelius'); + expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Hannah Arendt'), 'Hannah Arendt'); + expect(source.getAlbumDisplayName(context, '${FakeStorageService.primaryPath}Pictures/Arendt'), 'Arendt'); return const Placeholder(); }, ), diff --git a/test/utils/android_file_utils.dart b/test/utils/android_file_utils.dart new file mode 100644 index 000000000..66ef35d74 --- /dev/null +++ b/test/utils/android_file_utils.dart @@ -0,0 +1,27 @@ +import 'package:aves/services/services.dart'; +import 'package:aves/services/storage_service.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import '../fake/storage_service.dart'; + +void main() { + setUp(() async { + // specify Posix style path context for consistent behaviour when running tests on Windows + getIt.registerLazySingleton(() => p.Context(style: p.Style.posix)); + + getIt.registerLazySingleton(() => FakeStorageService()); + + await androidFileUtils.init(); + }); + + tearDown(() async { + await getIt.reset(); + }); + + test('camera album identification', () { + expect(androidFileUtils.isCameraPath('${FakeStorageService.primaryPath}DCIM/Camera'), true); + expect(androidFileUtils.isCameraPath('${FakeStorageService.primaryPath}DCIM/YoloCamera'), false); + }); +}