add android support for deleting medium and listing media with light weight option

This commit is contained in:
Wenqi Li 2023-07-24 00:33:23 +08:00
parent cbe3150ef0
commit 2db4d8901e
2 changed files with 308 additions and 24 deletions

View File

@ -1,5 +1,7 @@
package com.morbit.photogallery package com.morbit.photogallery
import android.app.Activity
import android.app.RecoverableSecurityException
import android.content.ContentResolver import android.content.ContentResolver
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
@ -11,6 +13,8 @@ import android.os.Build
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Size import android.util.Size
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.ActivityPluginBinding
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.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
@ -19,11 +23,12 @@ import io.flutter.plugin.common.PluginRegistry.Registrar
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.File import java.io.File
import java.io.FileOutputStream import java.io.FileOutputStream
import java.util.Collections
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
/** PhotoGalleryPlugin */ /** PhotoGalleryPlugin */
class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler { class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
companion object { companion object {
// This static function is optional and equivalent to onAttachedToEngine. It supports the old // This static function is optional and equivalent to onAttachedToEngine. It supports the old
// pre-Flutter-1.12 Android projects. You are encouraged to continue supporting // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
@ -61,6 +66,15 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
MediaStore.Images.Media.DATE_MODIFIED MediaStore.Images.Media.DATE_MODIFIED
) )
val imageBriefMetadataProjection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.WIDTH,
MediaStore.Images.Media.HEIGHT,
MediaStore.Images.Media.ORIENTATION,
MediaStore.Images.Media.DATE_ADDED,
MediaStore.Images.Media.DATE_MODIFIED
)
val videoMetadataProjection = arrayOf( val videoMetadataProjection = arrayOf(
MediaStore.Video.Media._ID, MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.DISPLAY_NAME,
@ -73,16 +87,26 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
MediaStore.Video.Media.DATE_ADDED, MediaStore.Video.Media.DATE_ADDED,
MediaStore.Video.Media.DATE_MODIFIED MediaStore.Video.Media.DATE_MODIFIED
) )
val videoBriefMetadataProjection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.WIDTH,
MediaStore.Video.Media.HEIGHT,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.DATE_ADDED,
MediaStore.Video.Media.DATE_MODIFIED
)
} }
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel
private lateinit var context: Context private lateinit var context: Context
private var activity: Activity? = null
private val executor: ExecutorService = Executors.newSingleThreadExecutor() private val executor: ExecutorService = Executors.newSingleThreadExecutor()
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "photo_gallery") channel = MethodChannel(flutterPluginBinding.binaryMessenger, "photo_gallery")
val plugin = PhotoGalleryPlugin() val plugin = this
plugin.context = flutterPluginBinding.applicationContext plugin.context = flutterPluginBinding.applicationContext
channel.setMethodCallHandler(plugin) channel.setMethodCallHandler(plugin)
} }
@ -91,6 +115,22 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
channel.setMethodCallHandler(null) channel.setMethodCallHandler(null)
} }
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
this.activity = binding.activity;
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
this.activity = binding.activity;
}
override fun onDetachedFromActivity() {
this.activity = null
}
override fun onDetachedFromActivityForConfigChanges() {
this.activity = null
}
override fun onMethodCall(call: MethodCall, result: Result) { override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) { when (call.method) {
"listAlbums" -> { "listAlbums" -> {
@ -108,9 +148,10 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
val newest = call.argument<Boolean>("newest") val newest = call.argument<Boolean>("newest")
val skip = call.argument<Int>("skip") val skip = call.argument<Int>("skip")
val take = call.argument<Int>("take") val take = call.argument<Int>("take")
val lightWeight = call.argument<Boolean>("lightWeight")
executor.submit { executor.submit {
result.success( result.success(
listMedia(mediumType, albumId!!, newest!!, skip, take) listMedia(mediumType, albumId!!, newest!!, skip, take, lightWeight)
) )
} }
} }
@ -163,6 +204,16 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
} }
} }
"deleteMedium" -> {
val mediumId = call.argument<String>("mediumId")
val mediumType = call.argument<String>("mediumType")
executor.submit {
result.success(
deleteMedium(mediumId!!, mediumType)
)
}
}
"cleanCache" -> { "cleanCache" -> {
executor.submit { executor.submit {
result.success( result.success(
@ -311,20 +362,21 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
albumId: String, albumId: String,
newest: Boolean, newest: Boolean,
skip: Int?, skip: Int?,
take: Int? take: Int?,
lightWeight: Boolean? = false
): Map<String, Any?> { ): Map<String, Any?> {
return when (mediumType) { return when (mediumType) {
imageType -> { imageType -> {
listImages(albumId, newest, skip, take) listImages(albumId, newest, skip, take, lightWeight)
} }
videoType -> { videoType -> {
listVideos(albumId, newest, skip, take) listVideos(albumId, newest, skip, take, lightWeight)
} }
else -> { else -> {
val images = listImages(albumId, newest, null, null)["items"] as List<Map<String, Any?>> val images = listImages(albumId, newest, null, null, lightWeight)["items"] as List<Map<String, Any?>>
val videos = listVideos(albumId, newest, null, null)["items"] as List<Map<String, Any?>> val videos = listVideos(albumId, newest, null, null, lightWeight)["items"] as List<Map<String, Any?>>
val comparator = compareBy<Map<String, Any?>> { it["creationDate"] as Long } val comparator = compareBy<Map<String, Any?>> { it["creationDate"] as Long }
.thenBy { it["modifiedDate"] as Long } .thenBy { it["modifiedDate"] as Long }
var items = (images + videos).sortedWith(comparator) var items = (images + videos).sortedWith(comparator)
@ -345,15 +397,23 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
} }
} }
private fun listImages(albumId: String, newest: Boolean, skip: Int?, take: Int?): Map<String, Any?> { private fun listImages(
albumId: String,
newest: Boolean,
skip: Int?,
take: Int?,
lightWeight: Boolean? = false
): Map<String, Any?> {
val media = mutableListOf<Map<String, Any?>>() val media = mutableListOf<Map<String, Any?>>()
this.context.run { this.context.run {
val imageCursor = getImageCursor(albumId, newest, imageMetadataProjection, skip, take) val projection = if (lightWeight == true) imageBriefMetadataProjection else imageMetadataProjection
val imageCursor = getImageCursor(albumId, newest, projection, skip, take)
imageCursor?.use { cursor -> imageCursor?.use { cursor ->
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
media.add(getImageMetadata(cursor)) val metadata = if (lightWeight == true) getImageBriefMetadata(cursor) else getImageMetadata(cursor)
media.add(metadata)
} }
} }
} }
@ -364,15 +424,23 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
) )
} }
private fun listVideos(albumId: String, newest: Boolean, skip: Int?, take: Int?): Map<String, Any?> { private fun listVideos(
albumId: String,
newest: Boolean,
skip: Int?,
take: Int?,
lightWeight: Boolean? = false
): Map<String, Any?> {
val media = mutableListOf<Map<String, Any?>>() val media = mutableListOf<Map<String, Any?>>()
this.context.run { this.context.run {
val videoCursor = getVideoCursor(albumId, newest, videoMetadataProjection, skip, take) val projection = if (lightWeight == true) videoBriefMetadataProjection else videoMetadataProjection
val videoCursor = getVideoCursor(albumId, newest, projection, skip, take)
videoCursor?.use { cursor -> videoCursor?.use { cursor ->
while (cursor.moveToNext()) { while (cursor.moveToNext()) {
media.add(getVideoMetadata(cursor)) val metadata = if (lightWeight == true) getVideoBriefMetadata(cursor) else getVideoMetadata(cursor)
media.add(metadata)
} }
} }
} }
@ -684,7 +752,13 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
} }
} }
private fun getImageCursor(albumId: String, newest: Boolean, projection: Array<String>, skip: Int?, take: Int?): Cursor? { private fun getImageCursor(
albumId: String,
newest: Boolean,
projection: Array<String>,
skip: Int?,
take: Int?
): Cursor? {
this.context.run { this.context.run {
val isSelection = albumId != allAlbumId val isSelection = albumId != allAlbumId
val selection = if (isSelection) "${MediaStore.Images.Media.BUCKET_ID} = ?" else null val selection = if (isSelection) "${MediaStore.Images.Media.BUCKET_ID} = ?" else null
@ -730,7 +804,13 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
} }
} }
private fun getVideoCursor(albumId: String, newest: Boolean, projection: Array<String>, skip: Int?, take: Int?): Cursor? { private fun getVideoCursor(
albumId: String,
newest: Boolean,
projection: Array<String>,
skip: Int?,
take: Int?
): Cursor? {
this.context.run { this.context.run {
val isSelection = albumId != allAlbumId val isSelection = albumId != allAlbumId
val selection = if (isSelection) "${MediaStore.Video.Media.BUCKET_ID} = ?" else null val selection = if (isSelection) "${MediaStore.Video.Media.BUCKET_ID} = ?" else null
@ -950,6 +1030,38 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
) )
} }
private fun getImageBriefMetadata(cursor: Cursor): Map<String, Any?> {
val idColumn = cursor.getColumnIndex(MediaStore.Images.Media._ID)
val widthColumn = cursor.getColumnIndex(MediaStore.Images.Media.WIDTH)
val heightColumn = cursor.getColumnIndex(MediaStore.Images.Media.HEIGHT)
val orientationColumn = cursor.getColumnIndex(MediaStore.Images.Media.ORIENTATION)
val dateAddedColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)
val dateModifiedColumn = cursor.getColumnIndex(MediaStore.Images.Media.DATE_MODIFIED)
val id = cursor.getLong(idColumn)
val width = cursor.getLong(widthColumn)
val height = cursor.getLong(heightColumn)
val orientation = cursor.getLong(orientationColumn)
var dateAdded: Long? = null
if (cursor.getType(dateAddedColumn) == FIELD_TYPE_INTEGER) {
dateAdded = cursor.getLong(dateAddedColumn) * 1000
}
var dateModified: Long? = null
if (cursor.getType(dateModifiedColumn) == FIELD_TYPE_INTEGER) {
dateModified = cursor.getLong(dateModifiedColumn) * 1000
}
return mapOf(
"id" to id.toString(),
"mediumType" to imageType,
"width" to width,
"height" to height,
"orientation" to orientationDegree2Value(orientation),
"creationDate" to dateAdded,
"modifiedDate" to dateModified
)
}
private fun getVideoMetadata(cursor: Cursor): Map<String, Any?> { private fun getVideoMetadata(cursor: Cursor): Map<String, Any?> {
val idColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID) val idColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID)
val filenameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME) val filenameColumn = cursor.getColumnIndex(MediaStore.Video.Media.DISPLAY_NAME)
@ -994,6 +1106,38 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
) )
} }
private fun getVideoBriefMetadata(cursor: Cursor): Map<String, Any?> {
val idColumn = cursor.getColumnIndex(MediaStore.Video.Media._ID)
val widthColumn = cursor.getColumnIndex(MediaStore.Video.Media.WIDTH)
val heightColumn = cursor.getColumnIndex(MediaStore.Video.Media.HEIGHT)
val durationColumn = cursor.getColumnIndex(MediaStore.Video.Media.DURATION)
val dateAddedColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATE_ADDED)
val dateModifiedColumn = cursor.getColumnIndex(MediaStore.Video.Media.DATE_MODIFIED)
val id = cursor.getLong(idColumn)
val width = cursor.getLong(widthColumn)
val height = cursor.getLong(heightColumn)
val duration = cursor.getLong(durationColumn)
var dateAdded: Long? = null
if (cursor.getType(dateAddedColumn) == FIELD_TYPE_INTEGER) {
dateAdded = cursor.getLong(dateAddedColumn) * 1000
}
var dateModified: Long? = null
if (cursor.getType(dateModifiedColumn) == FIELD_TYPE_INTEGER) {
dateModified = cursor.getLong(dateModifiedColumn) * 1000
}
return mapOf(
"id" to id.toString(),
"mediumType" to videoType,
"width" to width,
"height" to height,
"duration" to duration,
"creationDate" to dateAdded,
"modifiedDate" to dateModified
)
}
private fun orientationDegree2Value(degree: Long): Int { private fun orientationDegree2Value(degree: Long): Int {
return when (degree) { return when (degree) {
0L -> 1 0L -> 1
@ -1014,6 +1158,148 @@ class PhotoGalleryPlugin : FlutterPlugin, MethodCallHandler {
} }
} }
private fun deleteMedium(mediumId: String, mediumType: String?) {
when (mediumType) {
imageType -> {
deleteImageMedium(mediumId)
}
videoType -> {
deleteVideoMedium(mediumId)
}
else -> {
deleteImageMedium(mediumId)
deleteVideoMedium(mediumId)
}
}
}
private fun deleteImageMedium(mediumId: String) {
this.context.run {
val selection = "${MediaStore.Images.Media._ID} = ?"
val selectionArgs = arrayOf(mediumId)
val imageCursor = this.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
null,
selection,
selectionArgs,
null
)
imageCursor?.use {
if (it.count > 0) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
val pendingIntent = MediaStore.createTrashRequest(
this.contentResolver,
Collections.singleton(
ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
mediumId.toLong()
)
),
true
)
activity?.startIntentSenderForResult(
pendingIntent.intentSender,
0,
null,
0,
0,
0
)
} else {
try {
this.contentResolver.delete(
ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
mediumId.toLong()
),
selection,
selectionArgs
)
} catch (e: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val securityException = e as? RecoverableSecurityException ?: throw e
val intentSender = securityException.userAction.actionIntent.intentSender
activity?.startIntentSenderForResult(
intentSender,
0,
null,
0,
0,
0
)
}
}
}
}
}
}
}
private fun deleteVideoMedium(mediumId: String) {
this.context.run {
val selection = "${MediaStore.Video.Media._ID} = ?"
val selectionArgs = arrayOf(mediumId)
val videoCursor = this.contentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
null,
selection,
selectionArgs,
null
)
videoCursor?.use {
if (it.count > 0) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
val pendingIntent = MediaStore.createTrashRequest(
this.contentResolver,
Collections.singleton(
ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
mediumId.toLong()
)
),
true
)
activity?.startIntentSenderForResult(
pendingIntent.intentSender,
0,
null,
0,
0,
0
)
} else {
try {
this.contentResolver.delete(
ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
mediumId.toLong()
),
selection,
selectionArgs
)
} catch (e: SecurityException) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val securityException = e as? RecoverableSecurityException ?: throw e
val intentSender = securityException.userAction.actionIntent.intentSender
activity?.startIntentSenderForResult(
intentSender,
0,
null,
0,
0,
0
)
}
}
}
}
}
}
}
private fun cleanCache() { private fun cleanCache() {
val cachePath = getCachePath() val cachePath = getCachePath()
cachePath.deleteRecursively() cachePath.deleteRecursively()

View File

@ -41,6 +41,7 @@ class PhotoGallery {
required Album album, required Album album,
int? skip, int? skip,
int? take, int? take,
bool? lightWeight,
}) async { }) async {
final json = await _channel.invokeMethod('listMedia', { final json = await _channel.invokeMethod('listMedia', {
'albumId': album.id, 'albumId': album.id,
@ -48,6 +49,7 @@ class PhotoGallery {
'newest': album.newest, 'newest': album.newest,
'skip': skip, 'skip': skip,
'take': take, 'take': take,
'lightWeight': lightWeight,
}); });
return MediaPage.fromJson(album, json); return MediaPage.fromJson(album, json);
} }
@ -65,18 +67,14 @@ class PhotoGallery {
} }
/// Delete medium by medium id /// Delete medium by medium id
static Future<bool> deleteMedium({ static Future<void> deleteMedium({
required String mediumId, required String mediumId,
MediumType? mediumType,
}) async { }) async {
if (!Platform.isIOS) { await _channel.invokeMethod('deleteMedium', {
throw UnsupportedError('This function is only available on iOS');
}
final result = await _channel.invokeMethod('deleteMedium', {
'mediumId': mediumId, 'mediumId': mediumId,
'mediumType': mediumTypeToJson(mediumType),
}); });
return result as bool;
} }
/// Get medium thumbnail by medium id /// Get medium thumbnail by medium id