diff --git a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java index fdea9c636..ebe81b15d 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channel/calls/ImageFileHandler.java @@ -9,7 +9,6 @@ import androidx.annotation.NonNull; import com.bumptech.glide.Glide; -import java.io.File; import java.util.List; import java.util.Map; @@ -59,9 +58,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { case "rotate": new Thread(() -> rotate(call, new MethodResultWrapper(result))).start(); break; - case "renameDirectory": - new Thread(() -> renameDirectory(call, new MethodResultWrapper(result))).start(); - break; default: result.notImplemented(); break; @@ -183,29 +179,4 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { } }); } - - private void renameDirectory(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - String dirPath = call.argument("path"); - String newName = call.argument("newName"); - if (dirPath == null || newName == null) { - result.error("renameDirectory-args", "failed because of missing arguments", null); - return; - } - if (!dirPath.endsWith(File.separator)) { - dirPath += File.separator; - } - - ImageProvider provider = new MediaStoreImageProvider(); - provider.renameDirectory(activity, dirPath, newName, new ImageProvider.AlbumRenameOpCallback() { - @Override - public void onSuccess(List> fieldsByEntry) { - result.success(fieldsByEntry); - } - - @Override - public void onFailure(Throwable throwable) { - result.error("renameDirectory-failure", "failed to rename directory", throwable.getMessage()); - } - }); - } } \ No newline at end of file diff --git a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java index 47b6422c0..3c9600d5c 100644 --- a/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java +++ b/android/app/src/main/java/deckers/thibault/aves/model/provider/ImageProvider.java @@ -17,20 +17,14 @@ import com.bumptech.glide.load.resource.bitmap.TransformationUtils; import com.commonsware.cwac.document.DocumentFileCompat; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; import deckers.thibault.aves.model.AvesImageEntry; import deckers.thibault.aves.utils.MetadataHelper; @@ -92,122 +86,6 @@ public abstract class ImageProvider { scanNewPath(context, newFile.getPath(), mimeType, callback); } - @SuppressWarnings("UnstableApiUsage") - public void renameDirectory(Context context, final String oldDirPath, String newDirName, final AlbumRenameOpCallback callback) { - DocumentFileCompat destinationDirDocFile = StorageUtils.createDirectoryIfAbsent(context, oldDirPath); - if (destinationDirDocFile == null) { - callback.onFailure(new Exception("failed to find directory at path=" + oldDirPath)); - return; - } - - // list entries with their content IDs before renaming - Uri[] baseContentUris = new Uri[]{MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStore.Video.Media.EXTERNAL_CONTENT_URI}; - Map>> entriesByBaseContentUri = Arrays.stream(baseContentUris).collect(Collectors.toMap( - uri -> uri, - uri -> listContentEntries(context, uri, oldDirPath) - )); - - // rename directory - boolean renamed; - try { - renamed = destinationDirDocFile.renameTo(newDirName); - } catch (FileNotFoundException e) { - callback.onFailure(new Exception("failed to rename to name=" + newDirName + " directory at path=" + oldDirPath, e)); - return; - } - if (!renamed) { - callback.onFailure(new Exception("failed to rename to name=" + newDirName + " directory at path=" + oldDirPath)); - return; - } - String newDirPath = new File(oldDirPath).getParent() + File.separator + newDirName + File.separator; - - // scan old paths for cleanup, and new paths to fetch content IDs - Collection>> scanFutures = new ArrayList<>(); - entriesByBaseContentUri.forEach((baseContentUri, entries) -> { - int count = entries.size(); - if (count > 0) { - String[] mimeTypes = new String[count]; - String[] oldPaths = new String[count]; - String[] newPaths = new String[count]; - Map oldContentIdByPath = new HashMap<>(); - Map>> scanFutureByPath = new HashMap<>(); - for (int i = 0; i < count; i++) { - Map entry = entries.get(i); - String displayName = (String) entry.get("displayName"); - String newPath = newDirPath + displayName; - mimeTypes[i] = (String) entry.get("mimeType"); - oldPaths[i] = oldDirPath + displayName; - newPaths[i] = newPath; - oldContentIdByPath.put(newPath, (Integer) entry.get("oldContentId")); - scanFutureByPath.put(newPath, SettableFuture.create()); - } - MediaScannerConnection.scanFile(context, oldPaths, mimeTypes, null); - MediaScannerConnection.scanFile(context, newPaths, mimeTypes, (path, rawUri) -> { - SettableFuture> future = scanFutureByPath.get(path); - if (future == null) { - Log.e(LOG_TAG, "no future for path=" + path); - return; - } - - if (rawUri == null) { - future.setException(new Exception("failed to get URI of item at path=" + path)); - return; - } - - // newURI is possibly a file media URI (e.g. "content://media/12a9-8b42/file/62872") - // but we need an image/video media URI (e.g. "content://media/external/images/media/62872") - long contentId = ContentUris.parseId(rawUri); - Uri contentUri = ContentUris.withAppendedId(baseContentUri, contentId); - Map newFields = new HashMap() {{ - put("path", path); - put("uri", contentUri.toString()); - put("contentId", contentId); - put("oldContentId", oldContentIdByPath.get(path)); - }}; - future.set(newFields); - }); - - scanFutures.addAll(scanFutureByPath.values()); - } - }); - - try { - callback.onSuccess(Futures.allAsList(scanFutures).get()); - } catch (ExecutionException | InterruptedException e) { - callback.onFailure(e); - } - } - - private List> listContentEntries(Context context, Uri contentUri, String dirPath) { - List> entries = new ArrayList<>(); - String[] projection = { - MediaStore.MediaColumns._ID, - MediaStore.MediaColumns.DISPLAY_NAME, - MediaStore.MediaColumns.MIME_TYPE, - }; - String selection = MediaStore.MediaColumns.DATA + " like ?"; - - try { - Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, new String[]{dirPath + "%"}, null); - if (cursor != null) { - int idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID); - int displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME); - int mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.MIME_TYPE); - while (cursor.moveToNext()) { - entries.add(new HashMap() {{ - put("oldContentId", cursor.getInt(idColumn)); - put("displayName", cursor.getString(displayNameColumn)); - put("mimeType", cursor.getString(mimeTypeColumn)); - }}); - } - cursor.close(); - } - } catch (Exception e) { - Log.e(LOG_TAG, "failed to list entries in contentUri=" + contentUri, e); - } - return entries; - } - public void rotate(final Context context, final String path, final Uri uri, final String mimeType, final boolean clockwise, final ImageOpCallback callback) { switch (mimeType) { case MimeTypes.JPEG: @@ -416,10 +294,4 @@ public abstract class ImageProvider { void onFailure(Throwable throwable); } - - public interface AlbumRenameOpCallback { - void onSuccess(List> fieldsByEntry); - - void onFailure(Throwable throwable); - } } diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 89855ce28..3ef329b02 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -9,6 +9,7 @@ import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/location.dart'; import 'package:aves/model/source/tag.dart'; +import 'package:aves/services/image_file_service.dart'; import 'package:event_bus/event_bus.dart'; import 'package:flutter/foundation.dart'; @@ -108,20 +109,53 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { } void updateAfterMove({ - @required Iterable entries, - @required Set fromAlbums, - @required String toAlbum, + @required List selection, @required bool copy, - }) { + @required String destinationAlbum, + @required Iterable movedOps, + }) async { + if (movedOps.isEmpty) return; + + final fromAlbums = {}; + final movedEntries = []; if (copy) { - addAll(entries); + movedOps.forEach((movedOp) { + final sourceUri = movedOp.uri; + final newFields = movedOp.newFields; + final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); + fromAlbums.add(sourceEntry.directory); + movedEntries.add(sourceEntry?.copyWith( + uri: newFields['uri'] as String, + path: newFields['path'] as String, + contentId: newFields['contentId'] as int, + dateModifiedSecs: newFields['dateModifiedSecs'] as int, + )); + }); + await metadataDb.saveEntries(movedEntries); + await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata)); + await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails)); + } else { + await Future.forEach(movedOps, (movedOp) async { + final sourceUri = movedOp.uri; + final newFields = movedOp.newFields; + final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); + if (entry != null) { + fromAlbums.add(entry.directory); + movedEntries.add(entry); + await moveEntry(entry, newFields); + } + }); + } + + if (copy) { + addAll(movedEntries); } else { cleanEmptyAlbums(fromAlbums); - addFolderPath({toAlbum}); + addFolderPath({destinationAlbum}); } updateAlbums(); invalidateFilterEntryCounts(); - eventBus.fire(EntryMovedEvent(entries)); + eventBus.fire(EntryMovedEvent(movedEntries)); } int count(CollectionFilter filter) { diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 8011b97da..f6fb234c5 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -194,19 +194,6 @@ class ImageFileService { } return {}; } - - static Future> renameDirectory(String path, String newName) async { - try { - final result = await platform.invokeMethod('renameDirectory', { - 'path': path, - 'newName': newName, - }); - return (result as List).cast(); - } on PlatformException catch (e) { - debugPrint('renameDirectory failed with code=${e.code}, exception=${e.message}, details=${e.details}'); - } - return []; - } } @immutable diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index f39b66ade..a02ee9890 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -1,8 +1,6 @@ import 'dart:async'; import 'package:aves/model/filters/album.dart'; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/android_app_service.dart'; @@ -108,7 +106,6 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin { selection: selection, opStream: ImageFileService.move(selection, copy: copy, destinationAlbum: destinationAlbum), onDone: (processed) async { - debugPrint('$runtimeType _moveSelection onDone'); final movedOps = processed.where((e) => e.success); final movedCount = movedOps.length; final selectionCount = selection.length; @@ -119,44 +116,12 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin { final count = movedCount; showFeedback(context, '${copy ? 'Copied' : 'Moved'} ${Intl.plural(count, one: '$count item', other: '$count items')}'); } - if (movedCount > 0) { - final fromAlbums = {}; - final movedEntries = []; - if (copy) { - movedOps.forEach((movedOp) { - final sourceUri = movedOp.uri; - final newFields = movedOp.newFields; - final sourceEntry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); - fromAlbums.add(sourceEntry.directory); - movedEntries.add(sourceEntry?.copyWith( - uri: newFields['uri'] as String, - path: newFields['path'] as String, - contentId: newFields['contentId'] as int, - dateModifiedSecs: newFields['dateModifiedSecs'] as int, - )); - }); - await metadataDb.saveEntries(movedEntries); - await metadataDb.saveMetadata(movedEntries.map((entry) => entry.catalogMetadata)); - await metadataDb.saveAddresses(movedEntries.map((entry) => entry.addressDetails)); - } else { - await Future.forEach(movedOps, (movedOp) async { - final sourceUri = movedOp.uri; - final newFields = movedOp.newFields; - final entry = selection.firstWhere((entry) => entry.uri == sourceUri, orElse: () => null); - if (entry != null) { - fromAlbums.add(entry.directory); - movedEntries.add(entry); - await source.moveEntry(entry, newFields); - } - }); - } - source.updateAfterMove( - entries: movedEntries, - fromAlbums: fromAlbums, - toAlbum: destinationAlbum, - copy: copy, - ); - } + await source.updateAfterMove( + selection: selection, + copy: copy, + destinationAlbum: destinationAlbum, + movedOps: movedOps, + ); collection.clearSelection(); collection.browse(); }, diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 2cd0145ae..d1f2ecfb0 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -1,6 +1,5 @@ import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; @@ -113,30 +112,34 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per if (!await checkStoragePermissionForAlbums(context, {album})) return; - final result = await ImageFileService.renameDirectory(album, newName); + final selection = source.rawEntries.where(filter.filter).toList(); + final destinationAlbum = path.join(path.dirname(album), newName); - final albumEntries = source.rawEntries.where(filter.filter); - final movedEntries = []; - await Future.forEach(result, (newFields) async { - final oldContentId = newFields['oldContentId']; - final entry = albumEntries.firstWhere((entry) => entry.contentId == oldContentId, orElse: () => null); - if (entry != null) { - movedEntries.add(entry); - await source.moveEntry(entry, newFields); - } - }); - final newAlbum = path.join(path.dirname(album), newName); - source.updateAfterMove( - entries: movedEntries, - fromAlbums: {album}, - toAlbum: newAlbum, - copy: false, + showOpReport( + context: context, + selection: selection, + opStream: ImageFileService.move(selection, copy: false, destinationAlbum: destinationAlbum), + onDone: (processed) async { + final movedOps = processed.where((e) => e.success); + final movedCount = movedOps.length; + final selectionCount = selection.length; + if (movedCount < selectionCount) { + final count = selectionCount - movedCount; + showFeedback(context, 'Failed to move ${Intl.plural(count, one: '$count item', other: '$count items')}'); + } else { + showFeedback(context, 'Done!'); + } + await source.updateAfterMove( + selection: selection, + copy: false, + destinationAlbum: destinationAlbum, + movedOps: movedOps, + ); + final newFilter = AlbumFilter(destinationAlbum, source.getUniqueAlbumName(destinationAlbum)); + settings.pinnedFilters = settings.pinnedFilters + ..remove(filter) + ..add(newFilter); + }, ); - final newFilter = AlbumFilter(newAlbum, source.getUniqueAlbumName(newAlbum)); - settings.pinnedFilters = settings.pinnedFilters - ..remove(filter) - ..add(newFilter); - - showFeedback(context, 'Done!'); } }