From 82a2afe28429e0d63e979121e765c7c0d1fdc240 Mon Sep 17 00:00:00 2001 From: cuong_nl Date: Thu, 30 May 2024 11:57:16 +0700 Subject: [PATCH 1/8] =?UTF-8?q?fix=20error:=20'when'=20expression=20must?= =?UTF-8?q?=20be=20exhaustive,=20add=20necessary=20'is=20Pending'=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt index 87473a5..6833fe0 100644 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt +++ b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt @@ -334,6 +334,10 @@ class ImageDownloaderPlugin : FlutterPlugin, ActivityAware, MethodCallHandler { channel.invokeMethod("onProgressUpdate", args) } } + // fix error + // 'when' expression must be exhaustive, add necessary 'is Pending', 'is Successful' branches or 'else' branch instead + is Downloader.DownloadStatus.Successful -> Log.d(LOGGER_TAG, "Successful") + is Downloader.DownloadStatus.Pending -> Log.d(LOGGER_TAG, "Pending") else -> throw AssertionError() } From f006e5bbc569371e954e29a155f8e4a0c5dc263d Mon Sep 17 00:00:00 2001 From: cuong_nl Date: Thu, 30 May 2024 12:19:30 +0700 Subject: [PATCH 2/8] fix crash: android.database.CursorIndexOutOfBoundsException --- .../imagedownloader/ImageDownloaderPlugin.kt | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt index 6833fe0..e640927 100644 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt +++ b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt @@ -33,6 +33,7 @@ import java.io.FileInputStream import java.net.URLConnection import java.text.SimpleDateFormat import java.util.* +import kotlin.random.Random class ImageDownloaderPlugin : FlutterPlugin, ActivityAware, MethodCallHandler { companion object { @@ -289,9 +290,12 @@ class ImageDownloaderPlugin : FlutterPlugin, ActivityAware, MethodCallHandler { val inPublicDir = call.argument("inPublicDir") ?: true val directoryType = call.argument("directory") ?: "DIRECTORY_DOWNLOADS" val subDirectory = call.argument("subDirectory") - val tempSubDirectory = subDirectory ?: SimpleDateFormat( - "yyyy-MM-dd.HH.mm.sss", Locale.getDefault() - ).format(Date()) + + // add a random number to avoid conflicts, because when i downloaded multiple files so fast, the last file overwrites the previous ones + val tempSubDirectory = subDirectory ?: "${SimpleDateFormat( + "yyyy-MM-dd.HH.mm.sss", + Locale.getDefault() + ).format(Date())}${Random.nextInt(1000)}" val directory = convertToDirectory(directoryType) @@ -358,6 +362,15 @@ class ImageDownloaderPlugin : FlutterPlugin, ActivityAware, MethodCallHandler { null ) } else { + if (!file.canRead()) { + // it appears that the file is not readable, might the device is busy ( this happens when I try downloaded multiple files so fast) + result.error( + "read_failed", + "Couldn't read ${file.absolutePath ?: tempSubDirectory} ", + null + ) + return@execute // return early + } val stream = BufferedInputStream(FileInputStream(file)) val mimeType = outputMimeType ?: URLConnection.guessContentTypeFromStream(stream) @@ -381,6 +394,12 @@ class ImageDownloaderPlugin : FlutterPlugin, ActivityAware, MethodCallHandler { .getMimeTypeFromExtension(newFile.extension) ?: "" val imageId = saveToDatabase(newFile, mimeType ?: newMimeType, inPublicDir) + if (imageId == "") { + // in case the file was still downloaded but not saved to the gallery content provider, i don't know how to handle this case, someone might help :)) + + // result.error("save_to_database_failed", "Couldn't save the file to the database.", null) + // return@execute + } result.success(imageId) } }) @@ -424,8 +443,12 @@ class ImageDownloaderPlugin : FlutterPlugin, ActivityAware, MethodCallHandler { null ).use { checkNotNull(it) { "${file.absolutePath} is not found." } - it.moveToFirst() - it.getString(it.getColumnIndex(MediaStore.Images.Media._ID)) + if (it.moveToFirst()) { + it.getString(it.getColumnIndex(MediaStore.Images.Media._ID)) + } else { + // it appears that the cursor is empty + "" + } } } else { val db = TemporaryDatabase(context) From 85797688152bf47f5409ecc768dfd39ca4fb5780 Mon Sep 17 00:00:00 2001 From: cuong_nl Date: Sat, 8 Jun 2024 10:49:16 +0700 Subject: [PATCH 3/8] fix bug permission android 13 and above --- .../ImageDownloaderPermissionListener.kt | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPermissionListener.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPermissionListener.kt index 92ffc45..f773c36 100644 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPermissionListener.kt +++ b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPermissionListener.kt @@ -6,10 +6,25 @@ import android.content.pm.PackageManager import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import io.flutter.plugin.common.PluginRegistry +import android.os.Build class ImageDownloaderPermissionListener(private val activity: Activity) : PluginRegistry.RequestPermissionsResultListener { + private val storagePermissions = mutableListOf().apply { + val readStoragePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } + + this.add(readStoragePermission) + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + this.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } + } + private val permissionRequestId: Int = 2578166 var callback: Callback? = null @@ -18,7 +33,7 @@ class ImageDownloaderPermissionListener(private val activity: Activity) : requestCode: Int, permissions: Array, grantResults: IntArray ): Boolean { - if (!isPermissionGranted(permissions)) { + if (!isStoragePermissionGranted()) { // when select deny. callback?.denied() return false @@ -37,21 +52,19 @@ class ImageDownloaderPermissionListener(private val activity: Activity) : } fun alreadyGranted(): Boolean { - val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) - - if (!isPermissionGranted(permissions)) { + if (!isStoragePermissionGranted()) { // Request authorization. User is not yet authorized. - ActivityCompat.requestPermissions(activity, permissions, permissionRequestId) + ActivityCompat.requestPermissions(activity, storagePermissions.toTypedArray(), permissionRequestId) return false } // User already has authorization. Or below Android6.0 return true } - private fun isPermissionGranted(permissions: Array) = permissions.none { - ContextCompat.checkSelfPermission( - activity, it - ) != PackageManager.PERMISSION_GRANTED + fun isStoragePermissionGranted(): Boolean { + return storagePermissions.all { + activity.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED + } } interface Callback { From 593e89fe91e16772ab1618603b07849b7e36a43f Mon Sep 17 00:00:00 2001 From: cuongnl Date: Wed, 3 Jul 2024 17:54:38 +0700 Subject: [PATCH 4/8] fix download feature for android 13&14 --- android/build.gradle | 1 + .../com/ko2ic/imagedownloader/Downloader.kt | 214 ------- .../ImageDownloaderPermissionListener.kt | 75 --- .../imagedownloader/ImageDownloaderPlugin.kt | 525 +++--------------- example/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 2 + example/lib/main.dart | 266 ++------- example/lib/open_app_setting_dialog.dart | 55 ++ example/lib/permission_utils.dart | 63 +++ example/pubspec.yaml | 5 + lib/image_downloader.dart | 23 +- pubspec.yaml | 5 + 12 files changed, 258 insertions(+), 978 deletions(-) delete mode 100644 android/src/main/kotlin/com/ko2ic/imagedownloader/Downloader.kt delete mode 100644 android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPermissionListener.kt create mode 100644 example/lib/open_app_setting_dialog.dart create mode 100644 example/lib/permission_utils.dart diff --git a/android/build.gradle b/android/build.gradle index 1db5d2f..e3dcf1b 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -42,4 +42,5 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'androidx.core:core-ktx:1.9.0' + implementation 'com.caverock:androidsvg-aar:1.4' } diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/Downloader.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/Downloader.kt deleted file mode 100644 index b31e317..0000000 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/Downloader.kt +++ /dev/null @@ -1,214 +0,0 @@ -package com.ko2ic.imagedownloader - -import android.annotation.SuppressLint -import android.app.DownloadManager -import android.app.DownloadManager.* -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.database.Cursor -import java.math.BigDecimal -import java.math.RoundingMode - -class Downloader(private val context: Context, private val request: Request) { - - private val manager: DownloadManager = - context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - - private var receiver: BroadcastReceiver? = null - - private var downloadId: Long? = null - - @SuppressLint("Range") - fun execute( - onNext: (DownloadStatus) -> Unit, - onError: (DownloadFailedException) -> Unit, - onComplete: () -> Unit - ) { - receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - intent ?: return - if (ACTION_DOWNLOAD_COMPLETE == intent.action) { - val id = intent.getLongExtra(EXTRA_DOWNLOAD_ID, -1) - resolveDownloadStatus(id, onNext, onError, onComplete) - } - } - } - context.registerReceiver(receiver, IntentFilter(ACTION_DOWNLOAD_COMPLETE)) - downloadId = manager.enqueue(request) - downloadId?.let { nullableDownloadId -> - Thread { - var downloading = true - - while (downloading) { - - val q = Query() - q.setFilterById(nullableDownloadId) - - val cursor = manager.query(q) - cursor.moveToFirst() - - if (cursor.count == 0) { - cursor.close() - break - } - - val downloadedBytes = cursor.getInt( - cursor.getColumnIndex(COLUMN_BYTES_DOWNLOADED_SO_FAR) - ) - val totalBytes = cursor.getInt(cursor.getColumnIndex(COLUMN_TOTAL_SIZE_BYTES)) - - when (cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS))) { - STATUS_SUCCESSFUL -> downloading = false - STATUS_FAILED -> downloading = false - } - - if (totalBytes == 0) { - break - } - - val progress = BigDecimal(downloadedBytes).divide( - BigDecimal(totalBytes), 2, RoundingMode.DOWN - ).multiply( - BigDecimal(100) - ).setScale(0, RoundingMode.DOWN) - - onNext( - DownloadStatus.Running( - createRequestResult(nullableDownloadId, cursor), progress.toInt() - ) - ) - - cursor.close() - Thread.sleep(200) - } - }.start() - } - } - - fun cancel() { - downloadId?.let { - manager.remove(it) - } - - receiver?.let { - context.unregisterReceiver(it) - receiver = null - } - } - - @SuppressLint("Range") - private fun resolveDownloadStatus( - id: Long, - onNext: (DownloadStatus) -> Unit, - onError: (DownloadFailedException) -> Unit, - onComplete: () -> Unit - ) { - val query = Query().apply { - setFilterById(id) - } - val cursor = manager.query(query) - if (cursor.moveToFirst()) { - val status = cursor.getInt(cursor.getColumnIndex(COLUMN_STATUS)) - val reason = cursor.getInt(cursor.getColumnIndex(COLUMN_REASON)) - val requestResult: RequestResult = createRequestResult(id, cursor) - when (status) { - STATUS_FAILED -> { - val failedReason = when (reason) { - ERROR_CANNOT_RESUME -> Pair( - "ERROR_CANNOT_RESUME", - "Some possibly transient error occurred but we can't resume the download." - ) - ERROR_DEVICE_NOT_FOUND -> Pair( - "ERROR_DEVICE_NOT_FOUND", "No external storage device was found." - ) - ERROR_FILE_ALREADY_EXISTS -> Pair( - "ERROR_FILE_ALREADY_EXISTS", - "The requested destination file already exists (the download manager will not overwrite an existing file)." - ) - ERROR_FILE_ERROR -> Pair( - "ERROR_FILE_ERROR", - "A storage issue arises which doesn't fit under any other error code." - ) - ERROR_HTTP_DATA_ERROR -> Pair( - "ERROR_HTTP_DATA_ERROR", - "An error receiving or processing data occurred at the HTTP level." - ) - ERROR_INSUFFICIENT_SPACE -> Pair( - "ERROR_INSUFFICIENT_SPACE", "There was insufficient storage space." - ) - ERROR_TOO_MANY_REDIRECTS -> Pair( - "ERROR_TOO_MANY_REDIRECTS", "There were too many redirects." - ) - ERROR_UNHANDLED_HTTP_CODE -> Pair( - "ERROR_UNHANDLED_HTTP_CODE", - "An HTTP code was received that download manager can't handle." - ) - ERROR_UNKNOWN -> Pair( - "ERROR_UNKNOWN", - "The download has completed with an error that doesn't fit under any other error code." - ) - in 400..599 -> Pair(reason.toString(), "HTTP status code error.") - else -> Pair(reason.toString(), "Unknown.") - } - onNext(DownloadStatus.Failed(requestResult, failedReason.first)) - cancel() - onError(DownloadFailedException(failedReason.first, failedReason.second)) - } - STATUS_PAUSED -> { - val pausedReason = when (reason) { - PAUSED_QUEUED_FOR_WIFI -> "PAUSED_QUEUED_FOR_WIFI" - PAUSED_UNKNOWN -> "PAUSED_UNKNOWN" - PAUSED_WAITING_FOR_NETWORK -> "PAUSED_WAITING_FOR_NETWORK" - PAUSED_WAITING_TO_RETRY -> "PAUSED_WAITING_TO_RETRY" - else -> "PAUSED_UNKNOWN" - } - onNext(DownloadStatus.Paused(requestResult, pausedReason)) - } - STATUS_PENDING -> { - onNext(DownloadStatus.Pending(requestResult)) - } - STATUS_SUCCESSFUL -> { - onNext(DownloadStatus.Successful(requestResult)) - onComplete() - receiver?.let { - context.unregisterReceiver(it) - } - } - } - } - cursor.close() - } - - @SuppressLint("Range") - private fun createRequestResult(id: Long, cursor: Cursor): RequestResult = RequestResult( - id = id, - remoteUri = cursor.getString(cursor.getColumnIndex(COLUMN_URI)), - localUri = cursor.getString(cursor.getColumnIndex(COLUMN_LOCAL_URI)), - mediaType = cursor.getString(cursor.getColumnIndex(COLUMN_MEDIA_TYPE)), - totalSize = cursor.getInt(cursor.getColumnIndex(COLUMN_TOTAL_SIZE_BYTES)), - title = cursor.getString(cursor.getColumnIndex(COLUMN_TITLE)), - description = cursor.getString(cursor.getColumnIndex(COLUMN_DESCRIPTION)) - ) - - sealed class DownloadStatus(val result: RequestResult) { - class Successful(result: RequestResult) : DownloadStatus(result) - class Running(result: RequestResult, val progress: Int) : DownloadStatus(result) - class Pending(result: RequestResult) : DownloadStatus(result) - class Paused(result: RequestResult, val reason: String) : DownloadStatus(result) - class Failed(result: RequestResult, val reason: String) : DownloadStatus(result) - } - - class DownloadFailedException(val code: String, message: String) : Throwable(message) -} - -data class RequestResult( - val id: Long, - val remoteUri: String, - val localUri: String?, - val mediaType: String?, - val totalSize: Int, - val title: String?, - val description: String? -) \ No newline at end of file diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPermissionListener.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPermissionListener.kt deleted file mode 100644 index f773c36..0000000 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPermissionListener.kt +++ /dev/null @@ -1,75 +0,0 @@ -package com.ko2ic.imagedownloader - -import android.Manifest -import android.app.Activity -import android.content.pm.PackageManager -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat -import io.flutter.plugin.common.PluginRegistry -import android.os.Build - -class ImageDownloaderPermissionListener(private val activity: Activity) : - PluginRegistry.RequestPermissionsResultListener { - - private val storagePermissions = mutableListOf().apply { - val readStoragePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - Manifest.permission.READ_MEDIA_IMAGES - } else { - Manifest.permission.READ_EXTERNAL_STORAGE - } - - this.add(readStoragePermission) - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { - this.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) - } - } - - private val permissionRequestId: Int = 2578166 - - var callback: Callback? = null - - override fun onRequestPermissionsResult( - requestCode: Int, permissions: Array, grantResults: IntArray - ): Boolean { - - if (!isStoragePermissionGranted()) { - // when select deny. - callback?.denied() - return false - } - when (requestCode) { - permissionRequestId -> { - if (alreadyGranted()) { - callback?.granted() - } else { - callback?.denied() - } - } - else -> return false - } - return true - } - - fun alreadyGranted(): Boolean { - if (!isStoragePermissionGranted()) { - // Request authorization. User is not yet authorized. - ActivityCompat.requestPermissions(activity, storagePermissions.toTypedArray(), permissionRequestId) - return false - } - // User already has authorization. Or below Android6.0 - return true - } - - fun isStoragePermissionGranted(): Boolean { - return storagePermissions.all { - activity.checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED - } - } - - interface Callback { - fun granted() - fun denied() - } -} - diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt index e640927..25a6578 100644 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt +++ b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt @@ -1,495 +1,126 @@ package com.ko2ic.imagedownloader -import android.annotation.SuppressLint -import android.app.Activity -import android.app.DownloadManager +import androidx.annotation.NonNull +import android.annotation.TargetApi import android.content.ContentValues import android.content.Context import android.content.Intent -import android.database.sqlite.SQLiteDatabase -import android.database.sqlite.SQLiteOpenHelper +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.MediaScannerConnection import android.net.Uri -import android.os.Build import android.os.Environment -import android.os.Handler -import android.os.Looper +import android.os.Build import android.provider.MediaStore -import android.util.Log -import android.webkit.MimeTypeMap -import androidx.core.content.FileProvider -import com.ko2ic.imagedownloader.ImageDownloaderPlugin.TemporaryDatabase.Companion.COLUMNS -import com.ko2ic.imagedownloader.ImageDownloaderPlugin.TemporaryDatabase.Companion.TABLE_NAME 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.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.BufferedInputStream import java.io.File import java.io.FileInputStream -import java.net.URLConnection +import java.io.IOException +import android.text.TextUtils +import android.webkit.MimeTypeMap +import java.io.OutputStream +import android.util.Log +import com.caverock.androidsvg.SVG +import android.graphics.Canvas +import java.io.FileOutputStream +import java.io.InputStream import java.text.SimpleDateFormat import java.util.* import kotlin.random.Random -class ImageDownloaderPlugin : FlutterPlugin, ActivityAware, MethodCallHandler { - companion object { - @JvmStatic - fun registerWith(registrar: Registrar) { - val activity = registrar.activity() ?: return - val context = registrar.context() - val applicationContext = context.applicationContext - val pluginInstance = ImageDownloaderPlugin() - pluginInstance.setup( - registrar.messenger(), applicationContext, activity, registrar, null - ) - } - - private const val CHANNEL = "plugins.ko2ic.com/image_downloader" - private const val LOGGER_TAG = "image_downloader" - } - - private lateinit var channel: MethodChannel - private lateinit var permissionListener: ImageDownloaderPermissionListener - private lateinit var pluginBinding: FlutterPlugin.FlutterPluginBinding - - private var activityBinding: ActivityPluginBinding? = null +class ImageDownloaderPlugin : FlutterPlugin, MethodCallHandler { + private lateinit var methodChannel: MethodChannel private var applicationContext: Context? = null - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - pluginBinding = binding - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - tearDown() - } - - override fun onAttachedToActivity(activityPluginBinding: ActivityPluginBinding) { - setup( - pluginBinding.binaryMessenger, - pluginBinding.applicationContext, - activityPluginBinding.activity, - null, - activityPluginBinding - ) - } - - override fun onDetachedFromActivity() { - tearDown() - } - - override fun onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity() - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - onAttachedToActivity(binding) - } - - private fun setup( - messenger: BinaryMessenger, - applicationContext: Context, - activity: Activity, - registrar: Registrar?, - activityBinding: ActivityPluginBinding? - ) { - this.applicationContext = applicationContext - channel = MethodChannel(messenger, CHANNEL) - channel.setMethodCallHandler(this) - permissionListener = ImageDownloaderPermissionListener(activity) - - if (registrar != null) { - // V1 embedding setup for activity listeners. - registrar.addRequestPermissionsResultListener(permissionListener) - } else { - // V2 embedding setup for activity listeners. - this.activityBinding = activityBinding - this.activityBinding?.addRequestPermissionsResultListener(permissionListener) - } - } - - private fun tearDown() { - activityBinding?.removeRequestPermissionsResultListener(permissionListener) - channel.setMethodCallHandler(null) - applicationContext = null + override fun onAttachedToEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + this.applicationContext = binding.applicationContext + methodChannel = MethodChannel(binding.binaryMessenger, "plugins.ko2ic.com/image_downloader") + methodChannel.setMethodCallHandler(this) } - private var inPublicDir: Boolean = true - - private var callback: CallbackImpl? = null - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + override fun onMethodCall(@NonNull call: MethodCall,@NonNull result: Result): Unit { when (call.method) { "downloadImage" -> { - inPublicDir = call.argument("inPublicDir") ?: true + val cachedPath = call.argument("cachedPath") - val permissionCallback = - applicationContext?.let { CallbackImpl(call, result, channel, it) } - this.callback = permissionCallback - if (inPublicDir) { - this.permissionListener.callback = permissionCallback - if (permissionListener.alreadyGranted()) { - permissionCallback?.granted() - } - } else { - permissionCallback?.granted() - } - } - "cancel" -> { - callback?.downloader?.cancel() - } - "open" -> { - open(call, result) + saveImageToGallery(cachedPath, result) } + else -> { - "findPath" -> { - val imageId = call.argument("imageId") - ?: throw IllegalArgumentException("imageId is required.") - val filePath = applicationContext?.let { findPath(imageId, it) } - result.success(filePath) } - "findName" -> { - val imageId = call.argument("imageId") - ?: throw IllegalArgumentException("imageId is required.") - val fileName = applicationContext?.let { findName(imageId, it) } - result.success(fileName) - } - "findByteSize" -> { - val imageId = call.argument("imageId") - ?: throw IllegalArgumentException("imageId is required.") - val fileSize = applicationContext?.let { findByteSize(imageId, it) } - result.success(fileSize) - } - "findMimeType" -> { - val imageId = call.argument("imageId") - ?: throw IllegalArgumentException("imageId is required.") - val mimeType = applicationContext?.let { findMimeType(imageId, it) } - result.success(mimeType) - } - else -> result.notImplemented() } } - private fun open(call: MethodCall, result: MethodChannel.Result) { - - val path = - call.argument("path") ?: throw IllegalArgumentException("path is required.") - - val file = File(path) - val intent = Intent(Intent.ACTION_VIEW) - - val fileExtension = MimeTypeMap.getFileExtensionFromUrl(file.path) - val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension) - - if (Build.VERSION.SDK_INT >= 24) { - val uri = applicationContext?.let { - FileProvider.getUriForFile( - it, "${applicationContext?.packageName}.image_downloader.provider", file - ) - } - intent.setDataAndType(uri, mimeType) - } else { - intent.setDataAndType(Uri.fromFile(file), mimeType) - } - - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - - val manager = applicationContext?.packageManager - if (manager != null) { - if (manager.queryIntentActivities(intent, 0).size == 0) { - result.error("preview_error", "This file is not supported for previewing", null) - } else { - applicationContext?.startActivity(intent) - } - } - - } - - private fun findPath(imageId: String, context: Context): String { - val data = findFileData(imageId, context) - return data.path - } - - private fun findName(imageId: String, context: Context): String { - val data = findFileData(imageId, context) - return data.name - } - - private fun findByteSize(imageId: String, context: Context): Int { - val data = findFileData(imageId, context) - return data.byteSize - } - - private fun findMimeType(imageId: String, context: Context): String { - val data = findFileData(imageId, context) - return data.mimeType + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + applicationContext = null + methodChannel.setMethodCallHandler(null); } - @SuppressLint("Range") - private fun findFileData(imageId: String, context: Context): FileData { - - if (inPublicDir) { - val contentResolver = context.contentResolver - return contentResolver.query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - null, - "${MediaStore.Images.Media._ID}=?", - arrayOf(imageId), - null - ).use { - checkNotNull(it) { "$imageId is an imageId that does not exist." } - it.moveToFirst() - val path = it.getString(it.getColumnIndex(MediaStore.Images.Media.DATA)) - val name = it.getString(it.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)) - val size = it.getInt(it.getColumnIndex(MediaStore.Images.Media.SIZE)) - val mimeType = it.getString(it.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)) - FileData(path = path, name = name, byteSize = size, mimeType = mimeType) - } - } else { - val db = TemporaryDatabase(context).readableDatabase - return db.query( - TABLE_NAME, - COLUMNS, - "${MediaStore.Images.Media._ID}=?", - arrayOf(imageId), - null, - null, - null, - null - ).use { - it.moveToFirst() - val path = it.getString(it.getColumnIndex(MediaStore.Images.Media.DATA)) - val name = it.getString(it.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)) - val size = it.getInt(it.getColumnIndex(MediaStore.Images.Media.SIZE)) - val mimeType = it.getString(it.getColumnIndex(MediaStore.Images.Media.MIME_TYPE)) - FileData(path = path, name = name, byteSize = size, mimeType = mimeType) - } + private fun sendBroadcast(context: Context, fileUri: Uri?) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE) + mediaScanIntent.data = fileUri + context.sendBroadcast(mediaScanIntent) } } - class CallbackImpl( - private val call: MethodCall, - private val result: MethodChannel.Result, - private val channel: MethodChannel, - private val context: Context - ) : ImageDownloaderPermissionListener.Callback { - - var downloader: Downloader? = null - - override fun granted() { - val url = - call.argument("url") ?: throw IllegalArgumentException("url is required.") + private fun saveImageToGallery( + svgCachedPath: String?, + result: Result + ) { + // check parameters + if (svgCachedPath == null) { + return result.error("parameters error", null, null) + } + // check applicationContext + val context = applicationContext + ?: return result.error("context is null", null, null) - val headers: Map? = call.argument>("headers") + try { + val svgFile: File = File(svgCachedPath) + val inputStream: InputStream = FileInputStream(svgFile) + val svg: SVG = SVG.getFromInputStream(inputStream) - val outputMimeType = call.argument("mimeType") - val inPublicDir = call.argument("inPublicDir") ?: true - val directoryType = call.argument("directory") ?: "DIRECTORY_DOWNLOADS" - val subDirectory = call.argument("subDirectory") + val bitmap: Bitmap = Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888) + val canvas: Canvas = Canvas(bitmap) + canvas.drawPicture(svg.renderToPicture()) - // add a random number to avoid conflicts, because when i downloaded multiple files so fast, the last file overwrites the previous ones - val tempSubDirectory = subDirectory ?: "${SimpleDateFormat( + val fileName = "${SimpleDateFormat( "yyyy-MM-dd.HH.mm.sss", Locale.getDefault() - ).format(Date())}${Random.nextInt(1000)}" - - val directory = convertToDirectory(directoryType) - - val uri = Uri.parse(url) - val request = DownloadManager.Request(uri) - - //request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - request.allowScanningByMediaScanner() - - if (headers != null) { - for ((key, value) in headers) { - request.addRequestHeader(key, value) - } - } - - if (inPublicDir) { - request.setDestinationInExternalPublicDir(directory, tempSubDirectory) - } else { - TemporaryDatabase(context).writableDatabase.delete(TABLE_NAME, null, null) - request.setDestinationInExternalFilesDir(context, directory, tempSubDirectory) - } - - val downloader = Downloader(context, request) - this.downloader = downloader - - downloader.execute(onNext = { - Log.d(LOGGER_TAG, it.result.toString()) - when (it) { - is Downloader.DownloadStatus.Failed -> Log.d(LOGGER_TAG, it.reason) - is Downloader.DownloadStatus.Paused -> Log.d(LOGGER_TAG, it.reason) - is Downloader.DownloadStatus.Running -> { - Log.d(LOGGER_TAG, it.progress.toString()) - val args = HashMap() - args["image_id"] = it.result.id.toString() - args["progress"] = it.progress - - val uiThreadHandler = Handler(Looper.getMainLooper()) - - uiThreadHandler.post { - channel.invokeMethod("onProgressUpdate", args) - } - } - // fix error - // 'when' expression must be exhaustive, add necessary 'is Pending', 'is Successful' branches or 'else' branch instead - is Downloader.DownloadStatus.Successful -> Log.d(LOGGER_TAG, "Successful") - is Downloader.DownloadStatus.Pending -> Log.d(LOGGER_TAG, "Pending") - else -> throw AssertionError() - } - - }, onError = { - result.error(it.code, it.message, null) - }, onComplete = { - - val file = if (inPublicDir) { - File("${Environment.getExternalStoragePublicDirectory(directory)}/$tempSubDirectory") - } else { - File("${context.getExternalFilesDir(directory)}/$tempSubDirectory") - } - - if (!file.exists()) { - result.error( - "save_error", - "Couldn't save ${file.absolutePath ?: tempSubDirectory} ", - null - ) - } else { - if (!file.canRead()) { - // it appears that the file is not readable, might the device is busy ( this happens when I try downloaded multiple files so fast) - result.error( - "read_failed", - "Couldn't read ${file.absolutePath ?: tempSubDirectory} ", - null - ) - return@execute // return early - } - val stream = BufferedInputStream(FileInputStream(file)) - val mimeType = - outputMimeType ?: URLConnection.guessContentTypeFromStream(stream) - - val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType) - - val fileName = when { - subDirectory != null -> subDirectory - extension != null -> "$tempSubDirectory.$extension" - else -> uri.lastPathSegment?.split("/")?.last() ?: "file" - } - - val newFile = if (inPublicDir) { - File("${Environment.getExternalStoragePublicDirectory(directory)}/$fileName") - } else { - File("${context.getExternalFilesDir(directory)}/$fileName") - } - - file.renameTo(newFile) - val newMimeType = mimeType ?: MimeTypeMap.getSingleton() - .getMimeTypeFromExtension(newFile.extension) ?: "" - val imageId = saveToDatabase(newFile, mimeType ?: newMimeType, inPublicDir) - - if (imageId == "") { - // in case the file was still downloaded but not saved to the gallery content provider, i don't know how to handle this case, someone might help :)) - - // result.error("save_to_database_failed", "Couldn't save the file to the database.", null) - // return@execute - } - result.success(imageId) + ).format(Date())}${Random.nextInt(1000)}.png" + + // Create the content values to hold the metadata + val contentValues: ContentValues = ContentValues() + contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png") + contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + + // Insert the image into the MediaStore + val uri = context.getContentResolver() + .insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + + uri?.let { + // Get the output stream for the file + val outputStream = context.getContentResolver().openOutputStream(uri) + // Compress the bitmap and write to the output stream + if (outputStream != null) { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + outputStream.flush() + outputStream.close() } - }) - } - - override fun denied() { - result.success(null) - } - - private fun convertToDirectory(directoryType: String): String { - return when (directoryType) { - "DIRECTORY_DOWNLOADS" -> Environment.DIRECTORY_DOWNLOADS - "DIRECTORY_PICTURES" -> Environment.DIRECTORY_PICTURES - "DIRECTORY_DCIM" -> Environment.DIRECTORY_DCIM - "DIRECTORY_MOVIES" -> Environment.DIRECTORY_MOVIES - else -> directoryType + sendBroadcast(context, uri) } - } - - @SuppressLint("Range") - private fun saveToDatabase(file: File, mimeType: String, inPublicDir: Boolean): String { - val path = file.absolutePath - val name = file.name - val size = file.length() - - val contentValues = ContentValues() - contentValues.put(MediaStore.Images.Media.MIME_TYPE, mimeType) - contentValues.put(MediaStore.Images.Media.DATA, path) - contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, name) - contentValues.put(MediaStore.Images.ImageColumns.SIZE, size) - if (inPublicDir) { - - context.contentResolver.insert( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues - ) - return context.contentResolver.query( - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - arrayOf(MediaStore.Images.Media._ID, MediaStore.Images.Media.DATA), - "${MediaStore.Images.Media.DATA}=?", - arrayOf(file.absolutePath), - null - ).use { - checkNotNull(it) { "${file.absolutePath} is not found." } - if (it.moveToFirst()) { - it.getString(it.getColumnIndex(MediaStore.Images.Media._ID)) - } else { - // it appears that the cursor is empty - "" - } - } - } else { - val db = TemporaryDatabase(context) - val allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz0123456789" - val id = (1..20).map { allowedChars.random() }.joinToString("") - contentValues.put(MediaStore.Images.Media._ID, id) - db.writableDatabase.insert(TABLE_NAME, null, contentValues) - return id - } - } - } - - private data class FileData( - val path: String, val name: String, val byteSize: Int, val mimeType: String - ) - - class TemporaryDatabase(context: Context) : - SQLiteOpenHelper(context, TABLE_NAME, null, DATABASE_VERSION) { - - - companion object { - - val COLUMNS = arrayOf( - MediaStore.Images.Media._ID, - MediaStore.Images.Media.MIME_TYPE, - MediaStore.Images.Media.DATA, - MediaStore.Images.ImageColumns.DISPLAY_NAME, - MediaStore.Images.ImageColumns.SIZE - ) - - private const val DATABASE_VERSION = 1 - const val TABLE_NAME = "image_downloader_temporary" - private const val DICTIONARY_TABLE_CREATE = - "CREATE TABLE " + TABLE_NAME + " (" + MediaStore.Images.Media._ID + " TEXT, " + MediaStore.Images.Media.MIME_TYPE + " TEXT, " + MediaStore.Images.Media.DATA + " TEXT, " + MediaStore.Images.ImageColumns.DISPLAY_NAME + " TEXT, " + MediaStore.Images.ImageColumns.SIZE + " INTEGER" + ");" - } - - override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { - } - override fun onCreate(db: SQLiteDatabase) { - db.execSQL(DICTIONARY_TABLE_CREATE) + return result.success("unknown") + } catch (e: Exception) { + return result.error(e.message ?: "unknow", null, null) } } -} +} \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 0cff978..0fa87ea 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -36,7 +36,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.ko2ic.imagedownloader.example" - minSdkVersion 16 + minSdkVersion flutter.minSdkVersion targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 21a415f..e36375d 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ --> + + runApp(MyApp()); @@ -13,26 +12,6 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _message = ""; - String _path = ""; - String _size = ""; - String _mimeType = ""; - File? _imageFile; - int _progress = 0; - - List _mulitpleFiles = []; - - @override - void initState() { - super.initState(); - - ImageDownloader.callback(onProgressUpdate: (String? imageId, int progress) { - setState(() { - _progress = progress; - }); - }); - } - @override Widget build(BuildContext context) { return MaterialApp( @@ -41,226 +20,47 @@ class _MyAppState extends State { title: const Text('Plugin example app'), ), body: Center( - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Progress: $_progress %'), - Text(_message), - Text(_size), - Text(_mimeType), - Text(_path), - _path == "" - ? Container() - : Builder( - builder: (context) => ElevatedButton( - onPressed: () async { - await ImageDownloader.open(_path) - .catchError((error) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar( - content: Text( - (error as PlatformException).message ?? ''), - )); - }); - }, - child: Text("Open"), - ), - ), - ElevatedButton( - onPressed: () { - ImageDownloader.cancel(); - }, - child: Text("Cancel"), - ), - ElevatedButton( - onPressed: () { - _downloadImage( - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/bigsize.jpg"); - }, - child: Text("default destination"), - ), - ElevatedButton( - onPressed: () { - _downloadImage( - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter.png", - destination: AndroidDestinationType.directoryPictures - ..inExternalFilesDir() - ..subDirectory("sample.gif"), - ); - }, - child: Text("custom destination(only android)"), - ), - ElevatedButton( - onPressed: () { - _downloadImage( - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter_no.png", - whenError: true); - }, - child: Text("404 error"), - ), - ElevatedButton( - onPressed: () { - _downloadImage( - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/sample.mkv", - whenError: true); - //_downloadImage("https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/sample.3gp"); - }, - child: Text("unsupported file error(only ios)"), - ), - ElevatedButton( - onPressed: () { - //_downloadImage("https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/sample.mp4"); - //_downloadImage("https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/sample.m4v"); - _downloadImage( - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/sample.mov"); - }, - child: Text("movie"), - ), - ElevatedButton( - onPressed: () async { - var list = [ - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/bigsize.jpg", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter.jpg", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/sample.HEIC", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter_transparent.png", - "https://raw.githubusercontent.com/wiki/ko2ic/flutter_google_ad_manager/images/sample.gif", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter_no.png", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter.png", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter_real_png.jpg", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/bigsize.jpg", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter.jpg", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter_transparent.png", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter_no.png", - "https://raw.githubusercontent.com/wiki/ko2ic/flutter_google_ad_manager/images/sample.gif", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter.png", - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/flutter_real_png.jpg", - ]; - - List files = []; - - for (var url in list) { - try { - final imageId = - await ImageDownloader.downloadImage(url); - final path = await ImageDownloader.findPath(imageId!); - files.add(File(path!)); - } catch (error) { - print(error); - } - } - setState(() { - _mulitpleFiles.addAll(files); - }); - }, - child: Text("multiple downlod"), - ), - ElevatedButton( - onPressed: () => _downloadImage( - "https://raw.githubusercontent.com/wiki/ko2ic/image_downloader/images/sample.webp", - outputMimeType: "image/png", - ), - child: Text("download webp(only Android)"), - ), - (_imageFile == null) ? Container() : Image.file(_imageFile!), - GridView.count( - crossAxisCount: 4, - shrinkWrap: true, - physics: BouncingScrollPhysics(), - children: List.generate(_mulitpleFiles.length, (index) { - return SizedBox( - width: 50, - height: 50, - child: Image.file(File(_mulitpleFiles[index].path)), - ); - }), - ), - ], - ), + child: ElevatedButton( + onPressed: () { + _downloadEmoji(context, "https://raw.githubusercontent.com/googlefonts/noto-emoji/main/svg/emoji_u1f600.svg",); + }, + child: Text("Download emoji"), ), ), ), ); } - Future _downloadImage( - String url, { - AndroidDestinationType? destination, - bool whenError = false, - String? outputMimeType, - }) async { - String? fileName; - String? path; - int? size; - String? mimeType; - try { - String? imageId; - - if (whenError) { - imageId = await ImageDownloader.downloadImage(url, - outputMimeType: outputMimeType) - .catchError((error) { - if (error is PlatformException) { - String? path = ""; - if (error.code == "404") { - print("Not Found Error."); - } else if (error.code == "unsupported_file") { - print("UnSupported FIle Error."); - path = error.details["unsupported_file_path"]; - } - setState(() { - _message = error.toString(); - _path = path ?? ''; - }); - } - - print(error); - }).timeout(Duration(seconds: 10), onTimeout: () { - print("timeout"); - return; - }); - } else { - if (destination == null) { - imageId = await ImageDownloader.downloadImage( - url, - outputMimeType: outputMimeType, - ); - } else { - imageId = await ImageDownloader.downloadImage( - url, - destination: destination, - outputMimeType: outputMimeType, - ); - } - } - - if (imageId == null) { - return; - } - fileName = await ImageDownloader.findName(imageId); - path = await ImageDownloader.findPath(imageId); - size = await ImageDownloader.findByteSize(imageId); - mimeType = await ImageDownloader.findMimeType(imageId); - } on PlatformException catch (error) { - setState(() { - _message = error.message ?? ''; - }); + void _downloadEmoji(BuildContext context, String emojiUrl) async { + var isPermissionGranted = + await PermissionUtils.checkPermissionAndRequestStoragePermission( + context); + if (!isPermissionGranted) { return; } - if (!mounted) return; - - setState(() { - var location = Platform.isAndroid ? "Directory" : "Photo Library"; - _message = 'Saved as "$fileName" in $location.\n'; - _size = 'size: $size'; - _mimeType = 'mimeType: $mimeType'; - _path = path ?? ''; + bool isDownloaded = false; + await ImageDownloader.downloadImage( + emojiUrl, + ).then((value) { + isDownloaded = true; + }).catchError((error) { + print("error: ${error.toString()}"); + isDownloaded = false; + }); - if (!_mimeType.contains("video")) { - _imageFile = File(path!); - } + if (!isDownloaded) { + Fluttertoast.showToast( + msg: "Failed to download", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); return; - }); + } + Fluttertoast.showToast( + msg: "Downloaded successfully", + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + ); } } diff --git a/example/lib/open_app_setting_dialog.dart b/example/lib/open_app_setting_dialog.dart new file mode 100644 index 0000000..b66dbd1 --- /dev/null +++ b/example/lib/open_app_setting_dialog.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:app_settings/app_settings.dart'; + +class OpenAppSettingDialog extends StatelessWidget { + + final Function()? goToSetting; + const OpenAppSettingDialog({ + super.key, + this.goToSetting, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text( + "Permission required", + style: const TextStyle( + color: Color(0xFF1A1B1D), + fontSize: 18, + fontWeight: FontWeight.w500, + fontFamily: 'Rubik', + decoration: TextDecoration.none, + ), + ), + content: Text( + "Please allow storage permission to download emoji.", + style: const TextStyle( + color: Color(0xFF1A1B1D), + fontSize: 14, + fontWeight: FontWeight.w400, + fontFamily: 'Rubik', + decoration: TextDecoration.none, + ), + ), + actions: [ + TextButton( + onPressed: () { + goToSetting?.call(); + AppSettings.openAppSettings(); + }, + child: Text( + "Go to setting", + style: const TextStyle( + color: Color(0xFF007AFF), + fontSize: 16, + fontWeight: FontWeight.w500, + fontFamily: 'Rubik', + decoration: TextDecoration.none, + ), + ), + ), + ], + ); + } +} \ No newline at end of file diff --git a/example/lib/permission_utils.dart b/example/lib/permission_utils.dart new file mode 100644 index 0000000..0385edc --- /dev/null +++ b/example/lib/permission_utils.dart @@ -0,0 +1,63 @@ +import 'dart:io'; + +import 'package:device_info/device_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:image_downloader_example/open_app_setting_dialog.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class PermissionUtils { + static Future checkPermissionAndRequestStoragePermission( + BuildContext context, {Function()? goToSettings}) async { + final storagePermission = await getStoragePermission(); + + if (await storagePermission.request().isGranted) { + return true; + } + var status = await storagePermission.request(); + if (status.isGranted) { + if (kDebugMode) { + print('Permission is granted'); + } + return true; + } else if (status.isDenied) { + if (kDebugMode) { + print('Permission is denied'); + } + return false; + } else { + if (kDebugMode) { + print('Permission is permanently denied'); + } + showGeneralDialog( + context: context, + transitionDuration: const Duration(milliseconds: 0), + pageBuilder: (context, animation1, animation2) { + return OpenAppSettingDialog(goToSetting: () { + goToSettings?.call(); + Navigator.of(context).pop(); + }); + }); + return false; + } + } + + static Future isStoragePermissionGranted() async { + final storagePermission = await getStoragePermission(); + return storagePermission.isGranted; + } + + static Future getStoragePermission() async { + if (Platform.isAndroid) { + var androidInfo = await DeviceInfoPlugin().androidInfo; + var sdkInt = androidInfo.version.sdkInt; + if (sdkInt >= 33) { + return Permission.photos; + } else { + return Permission.storage; + } + } else { + return Permission.photos; + } + } +} diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 31017f7..7752918 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -19,6 +19,11 @@ dev_dependencies: image_downloader: path: ../ + fluttertoast: ^8.2.6 + permission_handler: ^11.3.1 + sn_progress_dialog: ^1.1.4 + device_info: ^2.0.3 + app_settings: ^5.1.1 # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec diff --git a/lib/image_downloader.dart b/lib/image_downloader.dart index 100dbd8..6389e55 100644 --- a/lib/image_downloader.dart +++ b/lib/image_downloader.dart @@ -1,6 +1,8 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/services.dart'; +import 'package:flutter_cache_manager/flutter_cache_manager.dart'; /// Provide the function to save the image on the Internet to each devices. class ImageDownloader { @@ -28,14 +30,19 @@ class ImageDownloader { Map? headers, AndroidDestinationType? destination, }) async { - return _channel.invokeMethod('downloadImage', { - 'url': url, - 'mimeType': outputMimeType, - 'headers': headers, - 'inPublicDir': destination?._inPublicDir, - 'directory': destination?._directory, - 'subDirectory': destination?._subDirectory, - }); + if (Platform.isAndroid) { + final cachedFile = await DefaultCacheManager().getSingleFile(url, shouldResizeTo512: true); + return _channel.invokeMethod('downloadImage', { 'cachedPath': cachedFile.path }); + } else { + return _channel.invokeMethod('downloadImage', { + 'url': url, + 'mimeType': outputMimeType, + 'headers': headers, + 'inPublicDir': destination?._inPublicDir, + 'directory': destination?._directory, + 'subDirectory': destination?._subDirectory, + }); + } } /// You can get the progress with [onProgressUpdate]. diff --git a/pubspec.yaml b/pubspec.yaml index 9c73d3e..c99d1ff 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,11 @@ environment: dependencies: flutter: sdk: flutter + flutter_cache_manager: + git: + url: https://github.com/luongcuong244/flutter_cache_manager + path: flutter_cache_manager + ref: develop # For information on the generic Dart part of this file, see the # following page: https://www.dartlang.org/tools/pub/pubspec From aefae589864e72987a71dc0294f8c3fcdca5e232 Mon Sep 17 00:00:00 2001 From: cuongnl Date: Tue, 1 Apr 2025 14:06:55 +0700 Subject: [PATCH 5/8] update for flutter 3.27.2 --- android/build.gradle | 19 ++++-- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../imagedownloader/ImageDownloaderPlugin.kt | 1 - example/.flutter-plugins-dependencies | 2 +- example/android/app/build.gradle | 63 +++++++++---------- example/android/build.gradle | 29 +++++---- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 30 ++++++--- example/android/settings_aar.gradle | 1 - example/lib/permission_utils.dart | 2 +- example/pubspec.yaml | 2 +- 11 files changed, 84 insertions(+), 69 deletions(-) delete mode 100644 example/android/settings_aar.gradle diff --git a/android/build.gradle b/android/build.gradle index e3dcf1b..a3c4ceb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'com.ko2ic.imagedownloader' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.5.31' + ext.kotlin_version = '1.7.22' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.5.4' + classpath 'com.android.tools.build:gradle:8.3.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -25,13 +25,22 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' android { - compileSdkVersion 33 + compileSdkVersion 34 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = 17 + } sourceSets { main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - minSdkVersion 16 + minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { @@ -41,6 +50,6 @@ android { dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.core:core-ktx:1.13.1' implementation 'com.caverock:androidsvg-aar:1.4' } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index f230c86..f7963f0 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt index 25a6578..597ebc1 100644 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt +++ b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt @@ -18,7 +18,6 @@ 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 diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 23ba14a..2f8299c 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"image_downloader","path":"/Users/abdalqaderalnajjar/AndroidStudioProjects/image_downloader/","native_build":true,"dependencies":[]}],"android":[{"name":"image_downloader","path":"/Users/abdalqaderalnajjar/AndroidStudioProjects/image_downloader/","native_build":true,"dependencies":[]}],"macos":[],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"image_downloader","dependencies":[]}],"date_created":"2023-05-15 12:14:11.013646","version":"3.10.0"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"app_settings","path":"/Users/admin/.pub-cache/hosted/pub.dev/app_settings-5.1.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/Users/admin/.pub-cache/hosted/pub.dev/fluttertoast-8.2.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_downloader","path":"/Users/admin/Documents/MyProjects/image_downloader/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_apple","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite","path":"/Users/admin/.pub-cache/hosted/pub.dev/sqflite-2.3.3+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"app_settings","path":"/Users/admin/.pub-cache/hosted/pub.dev/app_settings-5.1.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/Users/admin/.pub-cache/hosted/pub.dev/fluttertoast-8.2.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_downloader","path":"/Users/admin/Documents/MyProjects/image_downloader/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_android-2.2.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_android","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_android-12.0.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite","path":"/Users/admin/.pub-cache/hosted/pub.dev/sqflite-2.3.3+1/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite","path":"/Users/admin/.pub-cache/hosted/pub.dev/sqflite-2.3.3+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_windows","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/Users/admin/.pub-cache/hosted/pub.dev/fluttertoast-8.2.12/","dependencies":[],"dev_dependency":false},{"name":"permission_handler_html","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"app_settings","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"fluttertoast","dependencies":[]},{"name":"image_downloader","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"sqflite","dependencies":[]}],"date_created":"2025-04-01 14:05:18.437405","version":"3.29.2","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 0fa87ea..7fcea9d 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -1,67 +1,66 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') +def localPropertiesFile = rootProject.file("local.properties") if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> + localPropertiesFile.withReader("UTF-8") { reader -> localProperties.load(reader) } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +def flutterVersionCode = localProperties.getProperty("flutter.versionCode") if (flutterVersionCode == null) { - flutterVersionCode = '1' + flutterVersionCode = "100" } -def flutterVersionName = localProperties.getProperty('flutter.versionName') +def flutterVersionName = localProperties.getProperty("flutter.versionName") if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = "1.0.0" } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" +//def keystoreProperties = new Properties() +//def keystorePropertiesFile = rootProject.file('key.properties') +//if (keystorePropertiesFile.exists()) { +// keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +//} android { - compileSdkVersion 33 + namespace = "com.ko2ic.imagedownloader.example" + compileSdk = 34 + ndkVersion = flutter.ndkVersion - sourceSets { - main.java.srcDirs += 'src/main/kotlin' + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.ko2ic.imagedownloader.example" - minSdkVersion flutter.minSdkVersion - targetSdkVersion 33 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + applicationId = "com.ko2ic.imagedownloader.example" + minSdk = 24 + targetSdk = 34 + versionCode = flutterVersionCode.toInteger() + versionName = flutterVersionName } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + signingConfig = signingConfigs.debug } } - namespace 'com.ko2ic.imagedownloader.example' - lint { - disable 'InvalidPackage' - } } flutter { - source '../..' + source = "../.." } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' diff --git a/example/android/build.gradle b/example/android/build.gradle index d02fac2..d869516 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,31 +1,30 @@ -buildscript { - ext.kotlin_version = '1.8.21' +allprojects { repositories { google() mavenCentral() } + subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" - dependencies { - classpath 'com.android.tools.build:gradle:7.4.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + afterEvaluate { + // check if `android` block is available and namespace isn't set + if(it.hasProperty('android') && it.android.namespace == null){ + def manifest = new XmlSlurper().parse(file(it.android.sourceSets.main.manifest.srcFile)) + def packageName = manifest.@package.text() + android.namespace= packageName + } + } } } -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' +rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } subprojects { - project.evaluationDependsOn(':app') + project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { delete rootProject.buildDir -} +} \ No newline at end of file diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 8049c68..7666e22 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 5a2f14f..05bf906 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -1,15 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.2.1" apply false + id "org.jetbrains.kotlin.android" version "2.1.0" apply false } + +include ":app" \ No newline at end of file diff --git a/example/android/settings_aar.gradle b/example/android/settings_aar.gradle deleted file mode 100644 index e7b4def..0000000 --- a/example/android/settings_aar.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':app' diff --git a/example/lib/permission_utils.dart b/example/lib/permission_utils.dart index 0385edc..31f7b67 100644 --- a/example/lib/permission_utils.dart +++ b/example/lib/permission_utils.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:device_info/device_info.dart'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:image_downloader_example/open_app_setting_dialog.dart'; diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 7752918..7073638 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -22,7 +22,7 @@ dev_dependencies: fluttertoast: ^8.2.6 permission_handler: ^11.3.1 sn_progress_dialog: ^1.1.4 - device_info: ^2.0.3 + device_info_plus: ^11.1.0 app_settings: ^5.1.1 # For information on the generic Dart part of this file, see the From f8bb780dc90f450c673c6b3221b7978630b9bfa3 Mon Sep 17 00:00:00 2001 From: cuongnl Date: Tue, 1 Apr 2025 15:48:01 +0700 Subject: [PATCH 6/8] fix download function --- .../ko2ic/imagedownloader/ImageDownloaderPlugin.kt | 11 ++++------- example/.flutter-plugins-dependencies | 2 +- example/lib/main.dart | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt index 597ebc1..a6bc5b0 100644 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt +++ b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt @@ -82,13 +82,10 @@ class ImageDownloaderPlugin : FlutterPlugin, MethodCallHandler { ?: return result.error("context is null", null, null) try { - val svgFile: File = File(svgCachedPath) - val inputStream: InputStream = FileInputStream(svgFile) - val svg: SVG = SVG.getFromInputStream(inputStream) - - val bitmap: Bitmap = Bitmap.createBitmap(512, 512, Bitmap.Config.ARGB_8888) - val canvas: Canvas = Canvas(bitmap) - canvas.drawPicture(svg.renderToPicture()) + val bitmap: Bitmap? = BitmapFactory.decodeFile(svgCachedPath) + if (bitmap == null) { + return result.error("Failed to decode image", null, null) + } val fileName = "${SimpleDateFormat( "yyyy-MM-dd.HH.mm.sss", diff --git a/example/.flutter-plugins-dependencies b/example/.flutter-plugins-dependencies index 2f8299c..58b7bae 100644 --- a/example/.flutter-plugins-dependencies +++ b/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"app_settings","path":"/Users/admin/.pub-cache/hosted/pub.dev/app_settings-5.1.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/Users/admin/.pub-cache/hosted/pub.dev/fluttertoast-8.2.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_downloader","path":"/Users/admin/Documents/MyProjects/image_downloader/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_apple","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite","path":"/Users/admin/.pub-cache/hosted/pub.dev/sqflite-2.3.3+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"app_settings","path":"/Users/admin/.pub-cache/hosted/pub.dev/app_settings-5.1.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/Users/admin/.pub-cache/hosted/pub.dev/fluttertoast-8.2.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_downloader","path":"/Users/admin/Documents/MyProjects/image_downloader/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_android-2.2.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_android","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_android-12.0.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite","path":"/Users/admin/.pub-cache/hosted/pub.dev/sqflite-2.3.3+1/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite","path":"/Users/admin/.pub-cache/hosted/pub.dev/sqflite-2.3.3+1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_windows","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.0/","dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/Users/admin/.pub-cache/hosted/pub.dev/fluttertoast-8.2.12/","dependencies":[],"dev_dependency":false},{"name":"permission_handler_html","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.1/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"app_settings","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"fluttertoast","dependencies":[]},{"name":"image_downloader","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"sqflite","dependencies":[]}],"date_created":"2025-04-01 14:05:18.437405","version":"3.29.2","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"app_settings","path":"/Users/admin/.pub-cache/hosted/pub.dev/app_settings-5.2.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/Users/admin/.pub-cache/hosted/pub.dev/fluttertoast-8.2.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_downloader","path":"/Users/admin/Documents/MyProjects/image_downloader/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_apple","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_apple-9.4.6/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/Users/admin/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"app_settings","path":"/Users/admin/.pub-cache/hosted/pub.dev/app_settings-5.2.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/Users/admin/.pub-cache/hosted/pub.dev/fluttertoast-8.2.12/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"image_downloader","path":"/Users/admin/Documents/MyProjects/image_downloader/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_android","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_android-2.2.16/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_android","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_android-12.1.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_android","path":"/Users/admin/.pub-cache/hosted/pub.dev/sqflite_android-2.4.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"app_settings","path":"/Users/admin/.pub-cache/hosted/pub.dev/app_settings-5.2.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.3/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_foundation-2.4.1/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqflite_darwin","path":"/Users/admin/.pub-cache/hosted/pub.dev/sqflite_darwin-2.4.2/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.3/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.3/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/Users/admin/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"permission_handler_windows","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_windows-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"device_info_plus","path":"/Users/admin/.pub-cache/hosted/pub.dev/device_info_plus-11.3.3/","dependencies":[],"dev_dependency":false},{"name":"fluttertoast","path":"/Users/admin/.pub-cache/hosted/pub.dev/fluttertoast-8.2.12/","dependencies":[],"dev_dependency":false},{"name":"permission_handler_html","path":"/Users/admin/.pub-cache/hosted/pub.dev/permission_handler_html-0.1.3+5/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"app_settings","dependencies":[]},{"name":"device_info_plus","dependencies":[]},{"name":"fluttertoast","dependencies":[]},{"name":"image_downloader","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"permission_handler","dependencies":["permission_handler_android","permission_handler_apple","permission_handler_html","permission_handler_windows"]},{"name":"permission_handler_android","dependencies":[]},{"name":"permission_handler_apple","dependencies":[]},{"name":"permission_handler_html","dependencies":[]},{"name":"permission_handler_windows","dependencies":[]},{"name":"sqflite","dependencies":["sqflite_android","sqflite_darwin"]},{"name":"sqflite_android","dependencies":[]},{"name":"sqflite_darwin","dependencies":[]}],"date_created":"2025-04-01 15:46:38.052343","version":"3.29.2","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 4315c60..c8c32b5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -22,7 +22,7 @@ class _MyAppState extends State { body: Center( child: ElevatedButton( onPressed: () { - _downloadEmoji(context, "https://raw.githubusercontent.com/googlefonts/noto-emoji/main/svg/emoji_u1f600.svg",); + _downloadEmoji(context, "https://www.gstatic.com/android/keyboard/emojikitchen/20210521/u1fa84/u1fa84_u1f600.png",); }, child: Text("Download emoji"), ), From e062faaa5d52540a061d9a2b77e1f8dd92660e02 Mon Sep 17 00:00:00 2001 From: cuongnl Date: Mon, 14 Apr 2025 09:23:50 +0700 Subject: [PATCH 7/8] fix download function --- .../imagedownloader/ImageDownloaderPlugin.kt | 62 ++++++++++++------- 1 file changed, 39 insertions(+), 23 deletions(-) diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt index a6bc5b0..6ded04e 100644 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt +++ b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt @@ -73,13 +73,11 @@ class ImageDownloaderPlugin : FlutterPlugin, MethodCallHandler { svgCachedPath: String?, result: Result ) { - // check parameters if (svgCachedPath == null) { return result.error("parameters error", null, null) } - // check applicationContext - val context = applicationContext - ?: return result.error("context is null", null, null) + + val context = applicationContext ?: return result.error("context is null", null, null) try { val bitmap: Bitmap? = BitmapFactory.decodeFile(svgCachedPath) @@ -92,31 +90,49 @@ class ImageDownloaderPlugin : FlutterPlugin, MethodCallHandler { Locale.getDefault() ).format(Date())}${Random.nextInt(1000)}.png" - // Create the content values to hold the metadata - val contentValues: ContentValues = ContentValues() - contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) - contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png") - contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + var uri: Uri? = null // ✅ Khai báo ở ngoài để dùng chung - // Insert the image into the MediaStore - val uri = context.getContentResolver() - .insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // Android 10 trở lên + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, fileName) + put(MediaStore.MediaColumns.MIME_TYPE, "image/png") + put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) + } - uri?.let { - // Get the output stream for the file - val outputStream = context.getContentResolver().openOutputStream(uri) - // Compress the bitmap and write to the output stream - if (outputStream != null) { - bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) - outputStream.flush() - outputStream.close() + uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + uri?.let { + context.contentResolver.openOutputStream(it)?.use { outputStream -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + } } - sendBroadcast(context, uri) + } else { + // Android 9 trở xuống + val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + if (!downloadsDir.exists()) downloadsDir.mkdirs() + + val imageFile = File(downloadsDir, fileName) + FileOutputStream(imageFile).use { fos -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) + } + + // Cập nhật MediaStore để ảnh hiển thị trong Gallery + val values = ContentValues().apply { + put(MediaStore.Images.Media.DATA, imageFile.absolutePath) + put(MediaStore.Images.Media.MIME_TYPE, "image/png") + } + context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + + uri = Uri.fromFile(imageFile) + } + + uri?.let { + sendBroadcast(context, it) } - return result.success("unknown") + return result.success("saved") } catch (e: Exception) { - return result.error(e.message ?: "unknow", null, null) + return result.error(e.message ?: "unknown error", null, null) } } } \ No newline at end of file From fe6b15b8837d06e3db29dc54ffeea002588a9e9f Mon Sep 17 00:00:00 2001 From: cuongnl Date: Mon, 14 Apr 2025 09:34:20 +0700 Subject: [PATCH 8/8] fix download function --- .../com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt index 6ded04e..048b9ae 100644 --- a/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt +++ b/android/src/main/kotlin/com/ko2ic/imagedownloader/ImageDownloaderPlugin.kt @@ -100,10 +100,12 @@ class ImageDownloaderPlugin : FlutterPlugin, MethodCallHandler { put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS) } - uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + uri = context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) uri?.let { context.contentResolver.openOutputStream(it)?.use { outputStream -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream) + outputStream.flush() + outputStream.close() } } } else { @@ -114,6 +116,8 @@ class ImageDownloaderPlugin : FlutterPlugin, MethodCallHandler { val imageFile = File(downloadsDir, fileName) FileOutputStream(imageFile).use { fos -> bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) + fos.flush() + fos.close() } // Cập nhật MediaStore để ảnh hiển thị trong Gallery