From e3c5ba6b893e078beaac72fd3604437ca1a481a4 Mon Sep 17 00:00:00 2001 From: wenchieh Date: Thu, 9 Feb 2023 19:53:37 +0800 Subject: [PATCH 1/5] Compact Android scope storage --- android/build.gradle | 9 +- .../ImageGallerySaverPlugin.kt | 249 ++++++++++++++++-- example/android/app/build.gradle | 6 +- .../android/app/src/main/AndroidManifest.xml | 16 +- example/android/build.gradle | 4 +- example/lib/main.dart | 14 +- lib/image_gallery_saver.dart | 10 +- 7 files changed, 260 insertions(+), 48 deletions(-) diff --git a/android/build.gradle b/android/build.gradle index 9cceea9..18bea80 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,7 +5,7 @@ buildscript { ext.kotlin_version = '1.6.10' repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -17,7 +17,7 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -25,13 +25,14 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 30 + compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 16 + minSdkVersion 21 + targetSdkVersion 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { diff --git a/android/src/main/kotlin/com/example/imagegallerysaver/ImageGallerySaverPlugin.kt b/android/src/main/kotlin/com/example/imagegallerysaver/ImageGallerySaverPlugin.kt index 49e6a5d..4e5c5d2 100644 --- a/android/src/main/kotlin/com/example/imagegallerysaver/ImageGallerySaverPlugin.kt +++ b/android/src/main/kotlin/com/example/imagegallerysaver/ImageGallerySaverPlugin.kt @@ -1,55 +1,89 @@ package com.example.imagegallerysaver +import android.annotation.TargetApi +import android.content.ContentResolver import android.content.ContentValues import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri -import android.os.Environment import android.os.Build +import android.os.Environment import android.provider.MediaStore +import android.text.TextUtils +import android.text.format.DateUtils +import android.webkit.MimeTypeMap import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry.Registrar import java.io.File import java.io.FileInputStream import java.io.IOException -import android.text.TextUtils -import android.webkit.MimeTypeMap - +import java.text.SimpleDateFormat +import java.util.* class ImageGallerySaverPlugin : FlutterPlugin, MethodCallHandler { private var applicationContext: Context? = null private var methodChannel: MethodChannel? = null - override fun onMethodCall(call: MethodCall, result: Result): Unit { + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when { call.method == "saveImageToGallery" -> { val image = call.argument("imageBytes") ?: return val quality = call.argument("quality") ?: return val name = call.argument("name") - - result.success(saveImageToGallery(BitmapFactory.decodeByteArray(image, 0, image.size), quality, name)) + val folder = call.argument("folder") + if (Build.VERSION.SDK_INT >= 29) { + result.success( + saveImageToGallery29( + applicationContext!!, + BitmapFactory.decodeByteArray(image, 0, image.size), + name ?: "", + folder = folder ?: "", + ), + ) + } else { + result.success( + saveImageToGallery( + BitmapFactory.decodeByteArray( + image, + 0, + image.size, + ), + quality, + name ?: "", + ), + ) + } } call.method == "saveFileToGallery" -> { val path = call.argument("file") ?: return val name = call.argument("name") - result.success(saveFileToGallery(path, name)) + val folder = call.argument("folder") + + if (Build.VERSION.SDK_INT >= 29) { + result.success( + saveFileToGallery29( + applicationContext!!, + path, + name ?: "", + folder ?: "", + ), + ) + } else { + result.success(saveFileToGallery(path, name)) + } } else -> result.notImplemented() } - } - private fun generateUri(extension: String = "", name: String? = null): Uri { var fileName = name ?: System.currentTimeMillis().toString() - + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { var uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI @@ -66,7 +100,8 @@ class ImageGallerySaverPlugin : FlutterPlugin, MethodCallHandler { } return applicationContext?.contentResolver?.insert(uri, values)!! } else { - val storePath = Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.DIRECTORY_PICTURES + val storePath = + Environment.getExternalStorageDirectory().absolutePath + File.separator + Environment.DIRECTORY_PICTURES val appDir = File(storePath) if (!appDir.exists()) { appDir.mkdir() @@ -79,16 +114,26 @@ class ImageGallerySaverPlugin : FlutterPlugin, MethodCallHandler { } private fun getMIMEType(extension: String): String? { - var type: String? = null; + var type: String? = null if (!TextUtils.isEmpty(extension)) { type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension.toLowerCase()) } return type } - private fun saveImageToGallery(bmp: Bitmap, quality: Int, name: String?): HashMap { + private fun saveImageToGallery( + bmp: Bitmap, + quality: Int, + name: String, + ): HashMap { val context = applicationContext - val fileUri = generateUri("jpg", name = name) + val currentTime: Long = System.currentTimeMillis() + val imageDate: String = + SimpleDateFormat("yyyyMMdd-HHmmss", Locale.getDefault()).format(Date(currentTime)) + val screenshotFileNameTemplate = "%s.jpg" + val imageFileName: String = + name.ifEmpty { String.format(screenshotFileNameTemplate, imageDate) } + val fileUri = generateUri("jpg", name = imageFileName) return try { val fos = context?.contentResolver?.openOutputStream(fileUri)!! println("ImageGallerySaverPlugin $quality") @@ -103,6 +148,163 @@ class ImageGallerySaverPlugin : FlutterPlugin, MethodCallHandler { } } + /** + * android 10 以上版本 + */ + @TargetApi(Build.VERSION_CODES.Q) + fun saveImageToGallery29( + context: Context, + image: Bitmap, + name: String?, + folder: String = "", + ): HashMap { + val currentTime: Long = System.currentTimeMillis() + val imageDate: String = + SimpleDateFormat("yyyyMMdd-HHmmss", Locale.getDefault()).format(Date(currentTime)) + val screenshotFileNameTemplate = "%s.png" + val mImageFileName: String = name ?: String.format(screenshotFileNameTemplate, imageDate) + val values = ContentValues() + + values.put( + MediaStore.MediaColumns.RELATIVE_PATH, + if (folder.isEmpty()) { + Environment.DIRECTORY_PICTURES + } else { + "${Environment.DIRECTORY_PICTURES}${File.separator}$folder" + }, + ) + values.put(MediaStore.MediaColumns.DISPLAY_NAME, mImageFileName) + values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png") + values.put(MediaStore.MediaColumns.DATE_ADDED, currentTime / 1000) + values.put(MediaStore.MediaColumns.DATE_MODIFIED, currentTime / 1000) + values.put( + MediaStore.MediaColumns.DATE_EXPIRES, + (currentTime + DateUtils.DAY_IN_MILLIS) / 1000, + ) + values.put(MediaStore.MediaColumns.IS_PENDING, 1) + val resolver: ContentResolver = context.getContentResolver() + val uri: Uri? = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + if (uri == null) return SaveResultModel(false, null, "").toHashMap() + + try { + resolver.openOutputStream(uri).use { out -> + if (!image.compress(Bitmap.CompressFormat.PNG, 100, out)) { + return SaveResultModel(false, null, "").toHashMap() + } + } + values.clear() + values.put(MediaStore.MediaColumns.IS_PENDING, 0) + values.putNull(MediaStore.MediaColumns.DATE_EXPIRES) + resolver.update(uri, values, null, null) + } catch (e: IOException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + resolver.delete(uri, null) + } + return SaveResultModel(false, null, "").toHashMap() + } + return SaveResultModel(true, null, "").toHashMap() + } + + @TargetApi(Build.VERSION_CODES.Q) + private fun saveFileToGallery29( + context: Context, + filePath: String, + name: String, + folder: String, + ): HashMap { + var fileName = filePath + if (filePath.contains('/')) { + fileName = filePath.substringAfterLast("/") + } + var type = "png" + if (fileName.contains(".")) { + type = fileName.substringAfterLast(".") + } + val isImage = type.equals("png", ignoreCase = true) || + type.equals("webp", ignoreCase = true) || + type.equals("jpg", ignoreCase = true) || + type.equals("jpeg", ignoreCase = true) || + type.equals("heic", ignoreCase = true) + type.equals("gif", ignoreCase = true) + type.equals("apng", ignoreCase = true) + type.equals("raw", ignoreCase = true) + type.equals("svg", ignoreCase = true) + type.equals("bmp", ignoreCase = true) + type.equals("tif", ignoreCase = true) + + filePath.substringAfterLast(".") + val currentTime: Long = System.currentTimeMillis() + val imageDate: String = + SimpleDateFormat("yyyyMMdd-HHmmss", Locale.getDefault()).format(Date(currentTime)) + val screenshotFileNameTemplate = "%s.$type" + val mImageFileName: String = + name.ifEmpty { String.format(screenshotFileNameTemplate, imageDate) } + val values = ContentValues() + + values.put( + MediaStore.MediaColumns.RELATIVE_PATH, + if (folder.isEmpty()) { + if (isImage) { + Environment.DIRECTORY_PICTURES + } else { + Environment.DIRECTORY_MOVIES + } + } else { + if (isImage) { + "${Environment.DIRECTORY_PICTURES}${File.separator}$folder" + } else { + "${Environment.DIRECTORY_MOVIES}${File.separator}$folder" + } + }, + ) + values.put(MediaStore.MediaColumns.DISPLAY_NAME, mImageFileName) + try { + values.put( + MediaStore.MediaColumns.MIME_TYPE, + if (isImage) "image/$type" else "video/$type", + ) + } catch (e: java.lang.Exception) { + } + values.put(MediaStore.MediaColumns.DATE_ADDED, currentTime / 1000) + values.put(MediaStore.MediaColumns.DATE_MODIFIED, currentTime / 1000) + values.put( + MediaStore.MediaColumns.DATE_EXPIRES, + (currentTime + DateUtils.DAY_IN_MILLIS) / 1000, + ) + values.put(MediaStore.MediaColumns.IS_PENDING, 1) + val resolver: ContentResolver = context.getContentResolver() + val uri: Uri? = resolver.insert( + if (isImage) { + MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } else { + MediaStore.Video.Media.EXTERNAL_CONTENT_URI + }, + values, + ) + if (uri == null) return SaveResultModel(false, null, "").toHashMap() + try { + val fileInputStream = FileInputStream(filePath) + val data = ByteArray(1024) + var read = 0 + resolver.openOutputStream(uri).use { out -> + while ((fileInputStream.read(data, 0, data.size).also { read = it }) != -1) { + out?.write(data, 0, read) + } + } + values.clear() + values.put(MediaStore.MediaColumns.IS_PENDING, 0) + values.putNull(MediaStore.MediaColumns.DATE_EXPIRES) + fileInputStream.close() + resolver.update(uri, values, null, null) + } catch (e: IOException) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + resolver.delete(uri, null) + } + return SaveResultModel(false, null, "").toHashMap() + } + return SaveResultModel(true, null, "").toHashMap() + } + private fun saveFileToGallery(filePath: String, name: String?): HashMap { val context = applicationContext return try { @@ -135,8 +337,8 @@ class ImageGallerySaverPlugin : FlutterPlugin, MethodCallHandler { override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { applicationContext = null - methodChannel!!.setMethodCallHandler(null); - methodChannel = null; + methodChannel!!.setMethodCallHandler(null) + methodChannel = null } private fun onAttachedToEngine(applicationContext: Context, messenger: BinaryMessenger) { @@ -144,12 +346,13 @@ class ImageGallerySaverPlugin : FlutterPlugin, MethodCallHandler { methodChannel = MethodChannel(messenger, "image_gallery_saver") methodChannel!!.setMethodCallHandler(this) } - } -class SaveResultModel(var isSuccess: Boolean, - var filePath: String? = null, - var errorMessage: String? = null) { +class SaveResultModel( + var isSuccess: Boolean, + var filePath: String? = null, + var errorMessage: String? = null, +) { fun toHashMap(): HashMap { val hashMap = HashMap() hashMap["isSuccess"] = isSuccess diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 52f5373..64aa06e 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,7 +26,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 33 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 @@ -43,8 +43,8 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.imagegallerysaverexample" - minSdkVersion 16 - targetSdkVersion 29 + minSdkVersion 21 + targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index e2041eb..a48c5e6 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -5,7 +5,7 @@ flutter needs it to communicate with the running application to allow setting breakpoints, to provide hot reload, etc. --> - + + android:icon="@mipmap/ic_launcher" + android:label="image_gallery_saver_example">