check free space before move, copy & album renaming

This commit is contained in:
Thibault Deckers 2020-11-18 12:59:32 +09:00
parent 408744d286
commit dea00555e9
6 changed files with 147 additions and 20 deletions

View file

@ -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) {

View file

@ -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');

View file

@ -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,

View 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;
}
}

View file

@ -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;
}

View file

@ -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,