From d33e3af806101dade2c4cb061b26a114aa59e1c5 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Thu, 5 May 2022 00:54:20 +0800 Subject: [PATCH] Delete files via MediaStore api --- app/android/app/src/main/AndroidManifest.xml | 5 +- .../kotlin/com/nkming/nc_photos/plugin/K.kt | 2 + .../plugin/MediaStoreChannelHandler.kt | 69 ++++++++++++++++++- .../nkming/nc_photos/plugin/NcPhotosPlugin.kt | 43 +++++++++++- plugin/lib/src/media_store.dart | 36 ++++++++++ plugin/pubspec.lock | 7 ++ plugin/pubspec.yaml | 2 + 7 files changed, 160 insertions(+), 4 deletions(-) diff --git a/app/android/app/src/main/AndroidManifest.xml b/app/android/app/src/main/AndroidManifest.xml index 3f05186b..23237366 100644 --- a/app/android/app/src/main/AndroidManifest.xml +++ b/app/android/app/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ + android:largeHeap="true" + android:requestLegacyExternalStorage="true"> */ class MediaStoreChannelHandler(context: Context) : - MethodChannel.MethodCallHandler, ActivityAware { + MethodChannel.MethodCallHandler, EventChannel.StreamHandler, + ActivityAware, PluginRegistry.ActivityResultListener { companion object { + const val EVENT_CHANNEL = "${K.LIB_ID}/media_store" const val METHOD_CHANNEL = "${K.LIB_ID}/media_store_method" private const val TAG = "MediaStoreChannelHandler" @@ -49,6 +55,27 @@ class MediaStoreChannelHandler(context: Context) : activity = null } + override fun onActivityResult( + requestCode: Int, resultCode: Int, data: Intent? + ): Boolean { + if (requestCode == K.MEDIA_STORE_DELETE_REQUEST_CODE) { + eventSink?.success(buildMap { + put("event", "DeleteRequestResult") + put("resultCode", resultCode) + }) + return true + } + return false + } + + override fun onListen(arguments: Any?, events: EventChannel.EventSink) { + eventSink = events + } + + override fun onCancel(arguments: Any?) { + eventSink = null + } + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "saveFileToDownload" -> { @@ -81,6 +108,14 @@ class MediaStoreChannelHandler(context: Context) : } } + "deleteFiles" -> { + try { + deleteFiles(call.argument("uris")!!, result) + } catch (e: Throwable) { + result.error("systemException", e.message, null) + } + } + else -> result.notImplemented() } } @@ -196,6 +231,37 @@ class MediaStoreChannelHandler(context: Context) : result.success(files) } + private fun deleteFiles(uris: List, result: MethodChannel.Result) { + val urisTyped = uris.map(Uri::parse) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val pi = MediaStore.createDeleteRequest( + context.contentResolver, urisTyped + ) + activity!!.startIntentSenderForResult( + pi.intentSender, K.MEDIA_STORE_DELETE_REQUEST_CODE, null, 0, 0, + 0 + ) + result.success(null) + } else { + if (!PermissionUtil.hasWriteExternalStorage(context)) { + activity?.let { PermissionUtil.requestWriteExternalStorage(it) } + result.error("permissionError", "Permission not granted", null) + return + } + + val failed = mutableListOf() + for (uri in urisTyped) { + try { + context.contentResolver.delete(uri, null, null) + } catch (e: Throwable) { + Log.e(TAG, "[deleteFiles] Failed while delete", e) + failed += uri.toString() + } + } + result.success(failed) + } + } + private fun inputToUri(fromFile: String): Uri { val testUri = Uri.parse(fromFile) return if (testUri.scheme == null) { @@ -209,4 +275,5 @@ class MediaStoreChannelHandler(context: Context) : private val context = context private var activity: Activity? = null + private var eventSink: EventChannel.EventSink? = null } diff --git a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt index cbbcebac..95203f0a 100644 --- a/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt +++ b/plugin/android/src/main/kotlin/com/nkming/nc_photos/plugin/NcPhotosPlugin.kt @@ -1,17 +1,23 @@ package com.nkming.nc_photos.plugin +import android.content.Intent +import android.util.Log import androidx.annotation.NonNull import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.PluginRegistry -class NcPhotosPlugin : FlutterPlugin, ActivityAware { +class NcPhotosPlugin : FlutterPlugin, ActivityAware, + PluginRegistry.ActivityResultListener { companion object { const val ACTION_SHOW_IMAGE_PROCESSOR_RESULT = K.ACTION_SHOW_IMAGE_PROCESSOR_RESULT const val EXTRA_IMAGE_RESULT_URI = K.EXTRA_IMAGE_RESULT_URI + + private const val TAG = "NcPhotosPlugin" } override fun onAttachedToEngine( @@ -47,6 +53,11 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware { mediaStoreChannelHandler = MediaStoreChannelHandler(flutterPluginBinding.applicationContext) + mediaStoreChannel = EventChannel( + flutterPluginBinding.binaryMessenger, + MediaStoreChannelHandler.EVENT_CHANNEL + ) + mediaStoreChannel.setStreamHandler(mediaStoreChannelHandler) mediaStoreMethodChannel = MethodChannel( flutterPluginBinding.binaryMessenger, MediaStoreChannelHandler.METHOD_CHANNEL @@ -87,26 +98,56 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware { override fun onAttachedToActivity(binding: ActivityPluginBinding) { mediaStoreChannelHandler.onAttachedToActivity(binding) + pluginBinding = binding + binding.addActivityResultListener(this) } override fun onReattachedToActivityForConfigChanges( binding: ActivityPluginBinding ) { mediaStoreChannelHandler.onReattachedToActivityForConfigChanges(binding) + pluginBinding = binding + binding.addActivityResultListener(this) } override fun onDetachedFromActivity() { mediaStoreChannelHandler.onDetachedFromActivity() + pluginBinding?.removeActivityResultListener(this) } override fun onDetachedFromActivityForConfigChanges() { mediaStoreChannelHandler.onDetachedFromActivityForConfigChanges() + pluginBinding?.removeActivityResultListener(this) } + override fun onActivityResult( + requestCode: Int, resultCode: Int, data: Intent? + ): Boolean { + return try { + when (requestCode) { + K.MEDIA_STORE_DELETE_REQUEST_CODE -> { + mediaStoreChannelHandler.onActivityResult( + requestCode, resultCode, data + ) + } + + else -> false + } + } catch (e: Throwable) { + Log.e( + TAG, "Failed while onActivityResult, requestCode=$requestCode" + ) + false + } + } + + private var pluginBinding: ActivityPluginBinding? = null + private lateinit var lockChannel: MethodChannel private lateinit var notificationChannel: MethodChannel private lateinit var nativeEventChannel: EventChannel private lateinit var nativeEventMethodChannel: MethodChannel + private lateinit var mediaStoreChannel: EventChannel private lateinit var mediaStoreMethodChannel: MethodChannel private lateinit var imageProcessorMethodChannel: MethodChannel private lateinit var contentUriMethodChannel: MethodChannel diff --git a/plugin/lib/src/media_store.dart b/plugin/lib/src/media_store.dart index 72f9b89e..ce272841 100644 --- a/plugin/lib/src/media_store.dart +++ b/plugin/lib/src/media_store.dart @@ -1,6 +1,7 @@ import 'dart:typed_data'; import 'package:flutter/services.dart'; +import 'package:logging/logging.dart'; import 'package:nc_photos_plugin/src/exception.dart'; import 'package:nc_photos_plugin/src/k.dart' as k; @@ -16,6 +17,12 @@ class MediaStoreQueryResult { final int? dateTaken; } +class MediaStoreDeleteRequestResultEvent { + const MediaStoreDeleteRequestResultEvent(this.resultCode); + + final int resultCode; +} + class MediaStore { static Future saveFileToDownload( Uint8List content, @@ -85,7 +92,36 @@ class MediaStore { } } + static Future?> deleteFiles(List uris) async { + return (await _methodChannel + .invokeMethod("deleteFiles", { + "uris": uris, + })) + ?.cast(); + } + + static Stream get stream => _eventStream; + + static late final _eventStream = + _eventChannel.receiveBroadcastStream().map((event) { + if (event is Map) { + switch (event["event"]) { + case _eventDeleteRequestResult: + return MediaStoreDeleteRequestResultEvent(event["resultCode"]); + + default: + _log.shout("[_eventStream] Unknown event: ${event["event"]}"); + } + } else { + return event; + } + }); + + static const _eventChannel = EventChannel("${k.libId}/media_store"); static const _methodChannel = MethodChannel("${k.libId}/media_store_method"); static const _exceptionCodePermissionError = "permissionError"; + static const _eventDeleteRequestResult = "DeleteRequestResult"; + + static final _log = Logger("media_store.MediaStore"); } diff --git a/plugin/pubspec.lock b/plugin/pubspec.lock index 602a6ac4..8111b762 100644 --- a/plugin/pubspec.lock +++ b/plugin/pubspec.lock @@ -34,6 +34,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + logging: + dependency: "direct main" + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" material_color_utilities: dependency: transitive description: diff --git a/plugin/pubspec.yaml b/plugin/pubspec.yaml index 48d8becf..d9f61838 100644 --- a/plugin/pubspec.yaml +++ b/plugin/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: flutter: sdk: flutter + logging: ^1.0.2 + dev_dependencies: flutter_lints: ^1.0.0