check free space before move, copy & album renaming
This commit is contained in:
parent
408744d286
commit
dea00555e9
6 changed files with 147 additions and 20 deletions
|
@ -28,6 +28,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
}
|
||||
result.success(volumes)
|
||||
}
|
||||
"getFreeSpace" -> getFreeSpace(call, result)
|
||||
"getGrantedDirectories" -> result.success(ArrayList(PermissionManager.getGrantedDirs(context)))
|
||||
"getInaccessibleDirectories" -> getInaccessibleDirectories(call, result)
|
||||
"revokeDirectoryAccess" -> revokeDirectoryAccess(call, result)
|
||||
|
@ -62,6 +63,35 @@ class StorageHandler(private val context: Context) : MethodCallHandler {
|
|||
return volumes
|
||||
}
|
||||
|
||||
private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) {
|
||||
val path = call.argument<String>("path")
|
||||
if (path == null) {
|
||||
result.error("getFreeSpace-args", "failed because of missing arguments", null)
|
||||
return
|
||||
}
|
||||
|
||||
val sm = context.getSystemService(StorageManager::class.java)
|
||||
if (sm == null) {
|
||||
result.error("getFreeSpace-sm", "failed because of missing Storage Manager", null)
|
||||
return
|
||||
}
|
||||
|
||||
val file = File(path)
|
||||
val volume = sm.getStorageVolume(file)
|
||||
if (volume == null) {
|
||||
result.error("getFreeSpace-volume", "failed because of missing volume for path=$path", null)
|
||||
return
|
||||
}
|
||||
|
||||
// `StorageStatsManager` `getFreeBytes()` is only available from API 26,
|
||||
// and non-primary volume UUIDs cannot be used with it
|
||||
try {
|
||||
result.success(file.freeSpace)
|
||||
} catch (e: SecurityException) {
|
||||
result.error("getFreeSpace-security", "failed because of missing access", e.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) {
|
||||
val dirPaths = call.argument<List<String>>("dirPaths")
|
||||
if (dirPaths == null) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:streams_channel/streams_channel.dart';
|
||||
|
@ -18,6 +19,18 @@ class AndroidFileService {
|
|||
return [];
|
||||
}
|
||||
|
||||
static Future<int> getFreeSpace(StorageVolume volume) async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getFreeSpace', <String, dynamic>{
|
||||
'path': volume.path,
|
||||
});
|
||||
return result as int;
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint('getFreeSpace failed with code=${e.code}, exception=${e.message}, details=${e.details}}');
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static Future<List<String>> getGrantedDirectories() async {
|
||||
try {
|
||||
final result = await platform.invokeMethod('getGrantedDirectories');
|
||||
|
|
|
@ -12,6 +12,7 @@ import 'package:aves/widgets/collection/empty.dart';
|
|||
import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/feedback.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/size_aware.dart';
|
||||
import 'package:aves/widgets/common/aves_dialog.dart';
|
||||
import 'package:aves/widgets/common/entry_actions.dart';
|
||||
import 'package:aves/widgets/common/icons.dart';
|
||||
|
@ -25,7 +26,7 @@ import 'package:flutter/widgets.dart';
|
|||
import 'package:intl/intl.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||
class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
final CollectionLens collection;
|
||||
|
||||
SelectionActionDelegate({
|
||||
|
@ -116,6 +117,8 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
|||
final selection = collection.selection.toList();
|
||||
if (!await checkStoragePermission(context, selection)) return;
|
||||
|
||||
if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, copy)) return;
|
||||
|
||||
showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
selection: selection,
|
||||
|
|
51
lib/widgets/common/action_delegates/size_aware.dart
Normal file
51
lib/widgets/common/action_delegates/size_aware.dart
Normal file
|
@ -0,0 +1,51 @@
|
|||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/image_entry.dart';
|
||||
import 'package:aves/services/android_file_service.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/file_utils.dart';
|
||||
import 'package:aves/widgets/common/aves_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
mixin SizeAwareMixin {
|
||||
Future<bool> checkFreeSpaceForMove(BuildContext context, List<ImageEntry> selection, String destinationAlbum, bool copy) async {
|
||||
final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum);
|
||||
final free = await AndroidFileService.getFreeSpace(destinationVolume);
|
||||
int needed;
|
||||
int sumSize(sum, entry) => sum + entry.sizeBytes;
|
||||
if (copy) {
|
||||
needed = selection.fold(0, sumSize);
|
||||
} else {
|
||||
// when moving, we only need space for the entries that are not already on the destination volume
|
||||
final byVolume = groupBy<ImageEntry, StorageVolume>(selection, (entry) => androidFileUtils.getStorageVolume(entry.path));
|
||||
final otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume);
|
||||
final fromOtherVolumes = otherVolumes.fold<int>(0, (sum, volume) => sum + byVolume[volume].fold(0, sumSize));
|
||||
// and we need at least as much space as the largest entry because individual entries are copied then deleted
|
||||
final largestSingle = selection.fold<int>(0, (largest, entry) => max(largest, entry.sizeBytes));
|
||||
needed = max(fromOtherVolumes, largestSingle);
|
||||
}
|
||||
|
||||
final hasEnoughSpace = needed < free;
|
||||
if (!hasEnoughSpace) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AvesDialog(
|
||||
title: 'Not Enough Space',
|
||||
content: Text('This operation needs ${formatFilesize(needed)} of free space on “${destinationVolume.description}” to complete, but there is only ${formatFilesize(free)} left.'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text('OK'.toUpperCase()),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return hasEnoughSpace;
|
||||
}
|
||||
}
|
|
@ -1,32 +1,59 @@
|
|||
import 'package:aves/services/android_file_service.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/file_utils.dart';
|
||||
import 'package:aves/widgets/common/aves_expansion_tile.dart';
|
||||
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DebugStorageSection extends StatelessWidget {
|
||||
class DebugStorageSection extends StatefulWidget {
|
||||
@override
|
||||
_DebugStorageSectionState createState() => _DebugStorageSectionState();
|
||||
}
|
||||
|
||||
class _DebugStorageSectionState extends State<DebugStorageSection> with AutomaticKeepAliveClientMixin {
|
||||
final Map<String, int> _freeSpaceByVolume = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
androidFileUtils.storageVolumes.forEach((volume) async {
|
||||
final byteCount = await AndroidFileService.getFreeSpace(volume);
|
||||
setState(() => _freeSpaceByVolume[volume.path] = byteCount);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
return AvesExpansionTile(
|
||||
title: 'Storage Volumes',
|
||||
children: [
|
||||
...androidFileUtils.storageVolumes.expand((v) => [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Text(v.path),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: InfoRowGroup({
|
||||
'description': '${v.description}',
|
||||
'isEmulated': '${v.isEmulated}',
|
||||
'isPrimary': '${v.isPrimary}',
|
||||
'isRemovable': '${v.isRemovable}',
|
||||
'state': '${v.state}',
|
||||
}),
|
||||
),
|
||||
Divider(),
|
||||
])
|
||||
...androidFileUtils.storageVolumes.expand((v) {
|
||||
final freeSpace = _freeSpaceByVolume[v.path];
|
||||
return [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(8),
|
||||
child: Text(v.path),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: InfoRowGroup({
|
||||
'description': '${v.description}',
|
||||
'isEmulated': '${v.isEmulated}',
|
||||
'isPrimary': '${v.isPrimary}',
|
||||
'isRemovable': '${v.isRemovable}',
|
||||
'state': '${v.state}',
|
||||
if (freeSpace != null) 'freeSpace': formatFilesize(freeSpace),
|
||||
}),
|
||||
),
|
||||
Divider(),
|
||||
];
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import 'package:aves/utils/durations.dart';
|
|||
import 'package:aves/widgets/common/action_delegates/feedback.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/rename_album_dialog.dart';
|
||||
import 'package:aves/widgets/common/action_delegates/size_aware.dart';
|
||||
import 'package:aves/widgets/common/aves_dialog.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/chip_actions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -33,7 +34,7 @@ class ChipActionDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||
class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
final CollectionSource source;
|
||||
|
||||
AlbumChipActionDelegate({
|
||||
|
@ -113,6 +114,8 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per
|
|||
final selection = source.rawEntries.where(filter.filter).toList();
|
||||
final destinationAlbum = path.join(path.dirname(album), newName);
|
||||
|
||||
if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, false)) return;
|
||||
|
||||
showOpReport<MoveOpEvent>(
|
||||
context: context,
|
||||
selection: selection,
|
||||
|
|
Loading…
Reference in a new issue