From 1751b7b3d788c655ce23c4820b1a18a7f0cff3dc Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 24 Apr 2020 10:15:29 +0900 Subject: [PATCH] prep to delete multiple entries --- .../deckers/thibault/aves/MainActivity.java | 10 +- ...ndler.java => ImageByteStreamHandler.java} | 7 +- .../channelhandlers/ImageFileHandler.java | 45 ------- .../channelhandlers/ImageOpStreamHandler.java | 110 ++++++++++++++++++ .../MediaStoreStreamHandler.java | 1 - .../aves/model/provider/ImageProvider.java | 4 +- lib/model/image_entry.dart | 16 ++- lib/services/image_file_service.dart | 29 +++-- lib/widgets/album/thumbnail/decorated.dart | 30 ++--- lib/widgets/fullscreen/image_page.dart | 2 +- 10 files changed, 175 insertions(+), 79 deletions(-) rename android/app/src/main/java/deckers/thibault/aves/channelhandlers/{ImageStreamHandler.java => ImageByteStreamHandler.java} (96%) create mode 100644 android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageOpStreamHandler.java diff --git a/android/app/src/main/java/deckers/thibault/aves/MainActivity.java b/android/app/src/main/java/deckers/thibault/aves/MainActivity.java index b93b763bb..d5d3c0e15 100644 --- a/android/app/src/main/java/deckers/thibault/aves/MainActivity.java +++ b/android/app/src/main/java/deckers/thibault/aves/MainActivity.java @@ -11,8 +11,9 @@ import java.util.Map; import app.loup.streams_channel.StreamsChannel; import deckers.thibault.aves.channelhandlers.AppAdapterHandler; import deckers.thibault.aves.channelhandlers.FileAdapterHandler; +import deckers.thibault.aves.channelhandlers.ImageByteStreamHandler; import deckers.thibault.aves.channelhandlers.ImageFileHandler; -import deckers.thibault.aves.channelhandlers.ImageStreamHandler; +import deckers.thibault.aves.channelhandlers.ImageOpStreamHandler; import deckers.thibault.aves.channelhandlers.MediaStoreStreamHandler; import deckers.thibault.aves.channelhandlers.MetadataHandler; import deckers.thibault.aves.utils.Constants; @@ -48,8 +49,11 @@ public class MainActivity extends FlutterActivity { new MethodChannel(messenger, MetadataHandler.CHANNEL).setMethodCallHandler(new MetadataHandler(this)); new EventChannel(messenger, MediaStoreStreamHandler.CHANNEL).setStreamHandler(mediaStoreStreamHandler); - final StreamsChannel imageStreamChannel = new StreamsChannel(messenger, ImageStreamHandler.CHANNEL); - imageStreamChannel.setStreamHandlerFactory(arguments -> new ImageStreamHandler(this, arguments)); + final StreamsChannel imageByteStreamChannel = new StreamsChannel(messenger, ImageByteStreamHandler.CHANNEL); + imageByteStreamChannel.setStreamHandlerFactory(arguments -> new ImageByteStreamHandler(this, arguments)); + + final StreamsChannel imageOpStreamChannel = new StreamsChannel(messenger, ImageOpStreamHandler.CHANNEL); + imageOpStreamChannel.setStreamHandlerFactory(arguments -> new ImageOpStreamHandler(this, arguments)); new MethodChannel(messenger, VIEWER_CHANNEL).setMethodCallHandler( (call, result) -> { diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java similarity index 96% rename from android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageStreamHandler.java rename to android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java index aae7de800..88888dadb 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageStreamHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageByteStreamHandler.java @@ -24,8 +24,8 @@ import deckers.thibault.aves.decoder.VideoThumbnail; import deckers.thibault.aves.utils.MimeTypes; import io.flutter.plugin.common.EventChannel; -public class ImageStreamHandler implements EventChannel.StreamHandler { - public static final String CHANNEL = "deckers.thibault/aves/imagestream"; +public class ImageByteStreamHandler implements EventChannel.StreamHandler { + public static final String CHANNEL = "deckers.thibault/aves/imagebytestream"; private Activity activity; private Uri uri; @@ -33,7 +33,7 @@ public class ImageStreamHandler implements EventChannel.StreamHandler { private EventChannel.EventSink eventSink; private Handler handler; - public ImageStreamHandler(Activity activity, Object arguments) { + public ImageByteStreamHandler(Activity activity, Object arguments) { this.activity = activity; if (arguments instanceof Map) { Map argMap = (Map) arguments; @@ -87,7 +87,6 @@ public class ImageStreamHandler implements EventChannel.StreamHandler { } } catch (Exception e) { error("getImage-video-exception", "failed to get image from uri=" + uri, e.getMessage()); - return; } Glide.with(activity).clear(target); } else { diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java index 5e44b1495..3f83a0803 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageFileHandler.java @@ -7,7 +7,6 @@ import android.os.Looper; import androidx.annotation.NonNull; -import java.util.List; import java.util.Map; import deckers.thibault.aves.model.ImageEntry; @@ -40,9 +39,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { case "getThumbnail": new Thread(() -> getThumbnail(call, new MethodResultWrapper(result))).start(); break; - case "delete": - new Thread(() -> delete(call, new MethodResultWrapper(result))).start(); - break; case "rename": new Thread(() -> rename(call, new MethodResultWrapper(result))).start(); break; @@ -95,47 +91,6 @@ public class ImageFileHandler implements MethodChannel.MethodCallHandler { }); } - private void delete(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { - List entryMapList = call.argument("entries"); - if (entryMapList == null) { - result.error("delete-args", "failed because of missing arguments", null); - return; - } - - if (entryMapList.size() == 0) { - result.success(0); - return; - } - - // assume same provider for all entries - Map firstEntry = entryMapList.get(0); - Uri firstUri = Uri.parse((String) firstEntry.get("uri")); - ImageProvider provider = ImageProviderFactory.getProvider(firstUri); - if (provider == null) { - result.error("delete-provider", "failed to find provider for uri=" + firstUri, null); - return; - } - - for (Map entryMap : entryMapList) { - Uri uri = Uri.parse((String) entryMap.get("uri")); - String path = (String) entryMap.get("path"); - provider.delete(activity, path, uri, new ImageProvider.ImageOpCallback() { - - // TODO TLAD this will fail for more than 1 entry. stream results back instead - - @Override - public void onSuccess(Map newFields) { - new Handler(Looper.getMainLooper()).post(() -> result.success(1)); - } - - @Override - public void onFailure() { - new Handler(Looper.getMainLooper()).post(() -> result.error("delete-failure", "failed to delete", null)); - } - }); - } - } - private void rename(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { Map entryMap = call.argument("entry"); String newName = call.argument("newName"); diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageOpStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageOpStreamHandler.java new file mode 100644 index 000000000..d92c04ddc --- /dev/null +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/ImageOpStreamHandler.java @@ -0,0 +1,110 @@ +package deckers.thibault.aves.channelhandlers; + +import android.app.Activity; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import deckers.thibault.aves.model.provider.ImageProvider; +import deckers.thibault.aves.model.provider.ImageProviderFactory; +import io.flutter.plugin.common.EventChannel; + +public class ImageOpStreamHandler implements EventChannel.StreamHandler { + public static final String CHANNEL = "deckers.thibault/aves/imageopstream"; + + private Activity activity; + private EventChannel.EventSink eventSink; + private Handler handler; + private List entryMapList; + private String op; + + public ImageOpStreamHandler(Activity activity, Object arguments) { + this.activity = activity; + if (arguments instanceof Map) { + Map argMap = (Map) arguments; + this.op = (String) argMap.get("op"); + this.entryMapList = new ArrayList<>(); + List rawEntries = (List) argMap.get("entries"); + if (rawEntries != null) { + for (Object entry : rawEntries) { + entryMapList.add((Map) entry); + } + } + } + } + + @Override + public void onListen(Object o, final EventChannel.EventSink eventSink) { + this.eventSink = eventSink; + this.handler = new Handler(Looper.getMainLooper()); + if ("delete".equals(op)) { + new Thread(this::delete).start(); + } else { + endOfStream(); + } + } + + @Override + public void onCancel(Object o) { + } + + // {String uri, bool success} + private void success(final Map result) { + handler.post(() -> eventSink.success(result)); + } + + private void error(final String errorCode, final String errorMessage, final Object errorDetails) { + handler.post(() -> eventSink.error(errorCode, errorMessage, errorDetails)); + } + + private void endOfStream() { + handler.post(() -> eventSink.endOfStream()); + } + + private void delete() { + if (entryMapList.size() == 0) { + endOfStream(); + return; + } + + // assume same provider for all entries + Map firstEntry = entryMapList.get(0); + Uri firstUri = Uri.parse((String) firstEntry.get("uri")); + ImageProvider provider = ImageProviderFactory.getProvider(firstUri); + if (provider == null) { + error("delete-provider", "failed to find provider for uri=" + firstUri, null); + return; + } + + for (Map entryMap : entryMapList) { + String uriString = (String) entryMap.get("uri"); + Uri uri = Uri.parse(uriString); + String path = (String) entryMap.get("path"); + provider.delete(activity, path, uri, new ImageProvider.ImageOpCallback() { + @Override + public void onSuccess(Map newFields) { + Map result = new HashMap() {{ + put("uri", uriString); + put("success", true); + }}; + success(result); + } + + @Override + public void onFailure() { + Map result = new HashMap() {{ + put("uri", uriString); + put("success", false); + }}; + success(result); + } + }); + } + endOfStream(); + } +} diff --git a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java index 21717514b..eda16c48c 100644 --- a/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java +++ b/android/app/src/main/java/deckers/thibault/aves/channelhandlers/MediaStoreStreamHandler.java @@ -3,7 +3,6 @@ package deckers.thibault.aves.channelhandlers; import android.app.Activity; import android.os.Handler; import android.os.Looper; -import android.util.Log; import deckers.thibault.aves.model.provider.MediaStoreImageProvider; import deckers.thibault.aves.utils.Utils; 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 222bc32df..68490000c 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 @@ -307,9 +307,7 @@ public abstract class ImageProvider { values.put(MediaStore.MediaColumns.HEIGHT, rotatedHeight); int updatedRowCount = contentResolver.update(uri, values, null, null); if (updatedRowCount > 0) { - MediaScannerConnection.scanFile(activity, new String[]{path}, new String[]{mimeType}, (p, u) -> { - callback.onSuccess(newFields); - }); + MediaScannerConnection.scanFile(activity, new String[]{path}, new String[]{mimeType}, (p, u) -> callback.onSuccess(newFields)); } else { Log.w(LOG_TAG, "failed to update fields in Media Store for uri=" + uri); callback.onSuccess(newFields); diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 61ac31ab0..f7e92e486 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/image_metadata.dart'; import 'package:aves/services/image_file_service.dart'; @@ -295,7 +297,19 @@ class ImageEntry { return true; } - Future delete() async => (await ImageFileService.delete([this])) == 1; + Future delete() { + Completer completer = Completer(); + ImageFileService.delete([this]).listen( + (event) => completer.complete(event.success), + onError: completer.completeError, + onDone: () { + if (!completer.isCompleted) { + completer.complete(false); + } + }, + ); + return completer.future; + } void toggleFavourite() { if (isFavourite) { diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 87017b61e..8d940ffe5 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -10,7 +10,8 @@ import 'package:streams_channel/streams_channel.dart'; class ImageFileService { static const platform = MethodChannel('deckers.thibault/aves/image'); - static final StreamsChannel streamsChannel = StreamsChannel('deckers.thibault/aves/imagestream'); + static final StreamsChannel byteChannel = StreamsChannel('deckers.thibault/aves/imagebytestream'); + static final StreamsChannel opChannel = StreamsChannel('deckers.thibault/aves/imageopstream'); static Future getImageEntries() async { try { @@ -38,7 +39,7 @@ class ImageFileService { try { final completer = Completer(); final bytesBuilder = BytesBuilder(copy: false); - streamsChannel.receiveBroadcastStream({ + byteChannel.receiveBroadcastStream({ 'uri': uri, 'mimeType': mimeType, }).listen( @@ -77,16 +78,16 @@ class ImageFileService { ); } - static Future delete(List entries) async { + static Stream delete(List entries) { try { - await platform.invokeMethod('delete', { + return opChannel.receiveBroadcastStream({ + 'op': 'delete', 'entries': entries.map((e) => e.toMap()).toList(), - }); - return 1; + }).map((event) => ImageOpEvent.fromMap(event)); } on PlatformException catch (e) { debugPrint('delete failed with code=${e.code}, exception=${e.message}, details=${e.details}'); + return Stream.error(e); } - return 0; } static Future rename(ImageEntry entry, String newName) async { @@ -117,3 +118,17 @@ class ImageFileService { return {}; } } + +class ImageOpEvent { + final String uri; + final bool success; + + ImageOpEvent({this.uri, this.success}); + + factory ImageOpEvent.fromMap(Map map) { + return ImageOpEvent( + uri: map['uri'], + success: map['success'] ?? false, + ); + } +} diff --git a/lib/widgets/album/thumbnail/decorated.dart b/lib/widgets/album/thumbnail/decorated.dart index 8b63d29ba..a4731bcfc 100644 --- a/lib/widgets/album/thumbnail/decorated.dart +++ b/lib/widgets/album/thumbnail/decorated.dart @@ -45,22 +45,24 @@ class DecoratedThumbnail extends StatelessWidget { extent: extent, heroTag: heroTag, ), - if (showOverlay) Positioned( - bottom: 0, - left: 0, - child: ThumbnailEntryOverlay( - entry: entry, - extent: extent, + if (showOverlay) + Positioned( + bottom: 0, + left: 0, + child: ThumbnailEntryOverlay( + entry: entry, + extent: extent, + ), ), - ), - if (showOverlay) Positioned( - top: 0, - right: 0, - child: ThumbnailSelectionOverlay( - entry: entry, - extent: extent, + if (showOverlay) + Positioned( + top: 0, + right: 0, + child: ThumbnailSelectionOverlay( + entry: entry, + extent: extent, + ), ), - ), ], ), ); diff --git a/lib/widgets/fullscreen/image_page.dart b/lib/widgets/fullscreen/image_page.dart index b59d2a07e..c1cd3074a 100644 --- a/lib/widgets/fullscreen/image_page.dart +++ b/lib/widgets/fullscreen/image_page.dart @@ -1,8 +1,8 @@ import 'package:aves/model/collection_lens.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/fullscreen/image_view.dart'; -import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; import 'package:photo_view/photo_view.dart'; import 'package:tuple/tuple.dart';