Delete files via MediaStore api

This commit is contained in:
Ming Ming 2022-05-05 00:54:20 +08:00
parent 4da8b95c61
commit d33e3af806
7 changed files with 160 additions and 4 deletions

View file

@ -5,7 +5,7 @@
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" android:maxSdkVersion="29"
tools:ignore="ScopedStorage" /> tools:ignore="ScopedStorage" />
<application <application
@ -13,7 +13,8 @@
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config" android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="false" android:allowBackup="false"
android:largeHeap="true"> android:largeHeap="true"
android:requestLegacyExternalStorage="true">
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:launchMode="singleTop" android:launchMode="singleTop"

View file

@ -9,6 +9,8 @@ interface K {
const val IMAGE_PROCESSOR_SERVICE_RESULT_FAILED_NOTIFICATION_ID = 5002 const val IMAGE_PROCESSOR_SERVICE_RESULT_FAILED_NOTIFICATION_ID = 5002
const val PERMISSION_REQUEST_CODE = 11011 const val PERMISSION_REQUEST_CODE = 11011
const val MEDIA_STORE_DELETE_REQUEST_CODE = 11012
const val LIB_ID = "com.nkming.nc_photos.plugin" const val LIB_ID = "com.nkming.nc_photos.plugin"
const val ACTION_DOWNLOAD_CANCEL = "${LIB_ID}.ACTION_DOWNLOAD_CANCEL" const val ACTION_DOWNLOAD_CANCEL = "${LIB_ID}.ACTION_DOWNLOAD_CANCEL"

View file

@ -3,13 +3,17 @@ package com.nkming.nc_photos.plugin
import android.app.Activity import android.app.Activity
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Log
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
import java.io.File import java.io.File
/* /*
@ -24,8 +28,10 @@ import java.io.File
* fun queryFiles(relativePath: String): List<Map> * fun queryFiles(relativePath: String): List<Map>
*/ */
class MediaStoreChannelHandler(context: Context) : class MediaStoreChannelHandler(context: Context) :
MethodChannel.MethodCallHandler, ActivityAware { MethodChannel.MethodCallHandler, EventChannel.StreamHandler,
ActivityAware, PluginRegistry.ActivityResultListener {
companion object { companion object {
const val EVENT_CHANNEL = "${K.LIB_ID}/media_store"
const val METHOD_CHANNEL = "${K.LIB_ID}/media_store_method" const val METHOD_CHANNEL = "${K.LIB_ID}/media_store_method"
private const val TAG = "MediaStoreChannelHandler" private const val TAG = "MediaStoreChannelHandler"
@ -49,6 +55,27 @@ class MediaStoreChannelHandler(context: Context) :
activity = null 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) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) { when (call.method) {
"saveFileToDownload" -> { "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() else -> result.notImplemented()
} }
} }
@ -196,6 +231,37 @@ class MediaStoreChannelHandler(context: Context) :
result.success(files) result.success(files)
} }
private fun deleteFiles(uris: List<String>, 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<String>()
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 { private fun inputToUri(fromFile: String): Uri {
val testUri = Uri.parse(fromFile) val testUri = Uri.parse(fromFile)
return if (testUri.scheme == null) { return if (testUri.scheme == null) {
@ -209,4 +275,5 @@ class MediaStoreChannelHandler(context: Context) :
private val context = context private val context = context
private var activity: Activity? = null private var activity: Activity? = null
private var eventSink: EventChannel.EventSink? = null
} }

View file

@ -1,17 +1,23 @@
package com.nkming.nc_photos.plugin package com.nkming.nc_photos.plugin
import android.content.Intent
import android.util.Log
import androidx.annotation.NonNull import androidx.annotation.NonNull
import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.EventChannel import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.PluginRegistry
class NcPhotosPlugin : FlutterPlugin, ActivityAware { class NcPhotosPlugin : FlutterPlugin, ActivityAware,
PluginRegistry.ActivityResultListener {
companion object { companion object {
const val ACTION_SHOW_IMAGE_PROCESSOR_RESULT = const val ACTION_SHOW_IMAGE_PROCESSOR_RESULT =
K.ACTION_SHOW_IMAGE_PROCESSOR_RESULT K.ACTION_SHOW_IMAGE_PROCESSOR_RESULT
const val EXTRA_IMAGE_RESULT_URI = K.EXTRA_IMAGE_RESULT_URI const val EXTRA_IMAGE_RESULT_URI = K.EXTRA_IMAGE_RESULT_URI
private const val TAG = "NcPhotosPlugin"
} }
override fun onAttachedToEngine( override fun onAttachedToEngine(
@ -47,6 +53,11 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware {
mediaStoreChannelHandler = mediaStoreChannelHandler =
MediaStoreChannelHandler(flutterPluginBinding.applicationContext) MediaStoreChannelHandler(flutterPluginBinding.applicationContext)
mediaStoreChannel = EventChannel(
flutterPluginBinding.binaryMessenger,
MediaStoreChannelHandler.EVENT_CHANNEL
)
mediaStoreChannel.setStreamHandler(mediaStoreChannelHandler)
mediaStoreMethodChannel = MethodChannel( mediaStoreMethodChannel = MethodChannel(
flutterPluginBinding.binaryMessenger, flutterPluginBinding.binaryMessenger,
MediaStoreChannelHandler.METHOD_CHANNEL MediaStoreChannelHandler.METHOD_CHANNEL
@ -87,26 +98,56 @@ class NcPhotosPlugin : FlutterPlugin, ActivityAware {
override fun onAttachedToActivity(binding: ActivityPluginBinding) { override fun onAttachedToActivity(binding: ActivityPluginBinding) {
mediaStoreChannelHandler.onAttachedToActivity(binding) mediaStoreChannelHandler.onAttachedToActivity(binding)
pluginBinding = binding
binding.addActivityResultListener(this)
} }
override fun onReattachedToActivityForConfigChanges( override fun onReattachedToActivityForConfigChanges(
binding: ActivityPluginBinding binding: ActivityPluginBinding
) { ) {
mediaStoreChannelHandler.onReattachedToActivityForConfigChanges(binding) mediaStoreChannelHandler.onReattachedToActivityForConfigChanges(binding)
pluginBinding = binding
binding.addActivityResultListener(this)
} }
override fun onDetachedFromActivity() { override fun onDetachedFromActivity() {
mediaStoreChannelHandler.onDetachedFromActivity() mediaStoreChannelHandler.onDetachedFromActivity()
pluginBinding?.removeActivityResultListener(this)
} }
override fun onDetachedFromActivityForConfigChanges() { override fun onDetachedFromActivityForConfigChanges() {
mediaStoreChannelHandler.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 lockChannel: MethodChannel
private lateinit var notificationChannel: MethodChannel private lateinit var notificationChannel: MethodChannel
private lateinit var nativeEventChannel: EventChannel private lateinit var nativeEventChannel: EventChannel
private lateinit var nativeEventMethodChannel: MethodChannel private lateinit var nativeEventMethodChannel: MethodChannel
private lateinit var mediaStoreChannel: EventChannel
private lateinit var mediaStoreMethodChannel: MethodChannel private lateinit var mediaStoreMethodChannel: MethodChannel
private lateinit var imageProcessorMethodChannel: MethodChannel private lateinit var imageProcessorMethodChannel: MethodChannel
private lateinit var contentUriMethodChannel: MethodChannel private lateinit var contentUriMethodChannel: MethodChannel

View file

@ -1,6 +1,7 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos_plugin/src/exception.dart'; import 'package:nc_photos_plugin/src/exception.dart';
import 'package:nc_photos_plugin/src/k.dart' as k; import 'package:nc_photos_plugin/src/k.dart' as k;
@ -16,6 +17,12 @@ class MediaStoreQueryResult {
final int? dateTaken; final int? dateTaken;
} }
class MediaStoreDeleteRequestResultEvent {
const MediaStoreDeleteRequestResultEvent(this.resultCode);
final int resultCode;
}
class MediaStore { class MediaStore {
static Future<String> saveFileToDownload( static Future<String> saveFileToDownload(
Uint8List content, Uint8List content,
@ -85,7 +92,36 @@ class MediaStore {
} }
} }
static Future<List<String>?> deleteFiles(List<String> uris) async {
return (await _methodChannel
.invokeMethod<List>("deleteFiles", <String, dynamic>{
"uris": uris,
}))
?.cast<String>();
}
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 _methodChannel = MethodChannel("${k.libId}/media_store_method");
static const _exceptionCodePermissionError = "permissionError"; static const _exceptionCodePermissionError = "permissionError";
static const _eventDeleteRequestResult = "DeleteRequestResult";
static final _log = Logger("media_store.MediaStore");
} }

View file

@ -34,6 +34,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.1" 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: material_color_utilities:
dependency: transitive dependency: transitive
description: description:

View file

@ -11,6 +11,8 @@ dependencies:
flutter: flutter:
sdk: flutter sdk: flutter
logging: ^1.0.2
dev_dependencies: dev_dependencies:
flutter_lints: ^1.0.0 flutter_lints: ^1.0.0