import 'dart:async'; import 'package:aves/model/app_inventory.dart'; import 'package:aves/model/vaults/vaults.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves_model/aves_model.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; final AndroidFileUtils androidFileUtils = AndroidFileUtils._private(); class AndroidFileUtils { // cf https://developer.android.com/reference/android/content/ContentResolver#SCHEME_CONTENT static const contentScheme = 'content'; // cf https://developer.android.com/reference/android/provider/MediaStore#AUTHORITY static const mediaStoreAuthority = 'media'; // cf https://developer.android.com/reference/android/provider/MediaStore#VOLUME_EXTERNAL static const externalVolume = 'external'; static const standardDirDcim = 'DCIM'; static const standardDirDownloads = 'Download'; static const standardDirMovies = 'Movies'; static const standardDirPictures = 'Pictures'; static const mediaStoreUriRoot = '$contentScheme://$mediaStoreAuthority/'; static const mediaUriPathRoots = {'/$externalVolume/images/', '/$externalVolume/video/'}; static const recoveryDir = 'Lost & Found'; static const trashDirPath = '#trash'; late final String separator, vaultRoot, primaryStorage; late final String dcimPath, downloadPath, moviesPath, picturesPath, avesVideoCapturesPath; late final Set videoCapturesPaths; Set storageVolumes = {}; Future? _loader; AndroidFileUtils._private(); Future init() async { _loader ??= _doInit(); await _loader; } Future _doInit() async { separator = pContext.separator; await _initStorageVolumes(); vaultRoot = await storageService.getVaultRoot(); primaryStorage = storageVolumes.firstWhereOrNull((volume) => volume.isPrimary)?.path ?? separator; // standard dirs dcimPath = pContext.join(primaryStorage, standardDirDcim); // effective download path may have a different case downloadPath = pContext.join(primaryStorage, standardDirDownloads).toLowerCase(); moviesPath = pContext.join(primaryStorage, standardDirMovies); picturesPath = pContext.join(primaryStorage, standardDirPictures); // custom dirs avesVideoCapturesPath = pContext.join(dcimPath, 'Video Captures'); videoCapturesPaths = { // from Samsung pContext.join(dcimPath, 'Videocaptures'), // from Aves avesVideoCapturesPath, }; } Future _initStorageVolumes() async { storageVolumes = await storageService.getStorageVolumes(); if (storageVolumes.isEmpty) { // this can happen when the device is booting up debugPrint('Storage volume list is empty. Retrying in a second...'); await Future.delayed(const Duration(seconds: 1)); await _initStorageVolumes(); } } 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('${separator}Screenshots'); bool isScreenRecordingsPath(String path) => (path.startsWith(dcimPath) || path.startsWith(moviesPath)) && (path.endsWith('${separator}Screen recordings') || path.endsWith('${separator}ScreenRecords')); bool isVideoCapturesPath(String path) => videoCapturesPaths.contains(path); bool isDownloadPath(String path) => path.toLowerCase() == downloadPath; StorageVolume? getStorageVolume(String? path) { if (path == null) return null; 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(separator) ? volume : getStorageVolume('$path$separator'); } // prefer static method over a null returning factory constructor VolumeRelativeDirectory? relativeDirectoryFromPath(String dirPath) { final volume = getStorageVolume(dirPath); if (volume == null) return null; final root = volume.path; final rootLength = root.length; return VolumeRelativeDirectory( volumePath: root, relativeDir: dirPath.length < rootLength ? '' : dirPath.substring(rootLength), ); } bool isOnRemovableStorage(String path) => getStorageVolume(path)?.isRemovable ?? false; AlbumType getAlbumType(String dirPath) { if (vaults.isVault(dirPath)) return AlbumType.vault; if (isCameraPath(dirPath)) return AlbumType.camera; if (isDownloadPath(dirPath)) return AlbumType.download; if (isScreenRecordingsPath(dirPath)) return AlbumType.screenRecordings; if (isScreenshotsPath(dirPath)) return AlbumType.screenshots; if (isVideoCapturesPath(dirPath)) return AlbumType.videoCaptures; final dir = pContext.split(dirPath).lastOrNull; if (dir != null && dirPath.startsWith(primaryStorage) && appInventory.isPotentialAppDir(dir)) return AlbumType.app; return AlbumType.regular; } }