From c97a9256d495b598c7b7660620d4d3f476160a49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristian=20Men=C3=A1rguez=20Gonz=C3=A1lez?= Date: Wed, 27 Apr 2022 16:05:43 +0200 Subject: [PATCH] New feature multiple selection was added. New feature to select multiple files with the option multiplePicker, async task was changed by coroutine worker, this is the new reccommended way by google to make async task. Some code refactor. Commented // mCropProvider.delete() because, it was removing image after generate, it was causing an error that image wasn't available when client wanted to consumed. --- .idea/.gitignore | 3 + README.md | 4 ++ imagepicker/build.gradle | 1 + .../dhaval2404/imagepicker/ImagePicker.kt | 19 +++++ .../imagepicker/ImagePickerActivity.kt | 48 +++++++++---- .../exception/FailedToCompressException.kt | 4 ++ .../provider/CompressionProvider.kt | 70 ++++++++++++------- .../imagepicker/provider/CropProvider.kt | 19 ++--- .../imagepicker/provider/GalleryProvider.kt | 27 +++++-- .../imagepicker/util/ClipDataUtils.kt | 20 ++++++ .../imagepicker/util/IntentUtils.kt | 17 +++-- .../sample/MainActivity.kt | 9 ++- 12 files changed, 182 insertions(+), 59 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/exception/FailedToCompressException.kt create mode 100644 imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/ClipDataUtils.kt diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..26d33521 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/README.md b/README.md index f9766c15..c39dc3b2 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ Almost 90% of the app that I have developed has an Image upload feature. Along w ```kotlin ImagePicker.with(this) + .multiplePicker(true) // True or false if you want to pick multiple files, if multiplepicker / crop is disabled, please remember that legacy picker can't be use as multiple picker .crop() //Crop image(Optional), Check Customization for more option .compress(1024) //Final image size will be less than 1 MB(Optional) .maxResultSize(1080, 1080) //Final image resolution will be less than 1080 x 1080(Optional) @@ -96,8 +97,11 @@ Almost 90% of the app that I have developed has an Image upload feature. Along w override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) { + //Image Uri will not be null for RESULT_OK val uri: Uri = data?.data!! + // If multiple selector, uri will be null for RESULT_OK you have to access + // data?.extras?.getParcelableArray(ImagePicker.RESULT_MULTIPLE_FILES)?.map { it as Uri }?.toList()!! this return a multiple files selected // Use Uri object instead of File to avoid storage permissions imgProfile.setImageURI(fileUri) diff --git a/imagepicker/build.gradle b/imagepicker/build.gradle index 39aa0d0d..b0d7e458 100644 --- a/imagepicker/build.gradle +++ b/imagepicker/build.gradle @@ -44,6 +44,7 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' implementation "androidx.exifinterface:exifinterface:1.3.2" implementation 'androidx.documentfile:documentfile:1.0.1' diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePicker.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePicker.kt index fddfbe4c..d163a25b 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePicker.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePicker.kt @@ -23,6 +23,7 @@ open class ImagePicker { // Default Request Code to Pick Image const val REQUEST_CODE = 2404 const val RESULT_ERROR = 64 + const val RESULT_MULTIPLE_FILES = "extra.file_path.multiples" internal const val EXTRA_IMAGE_PROVIDER = "extra.image_provider" internal const val EXTRA_CAMERA_DEVICE = "extra.camera_device" @@ -33,6 +34,7 @@ open class ImagePicker { internal const val EXTRA_CROP_Y = "extra.crop_y" internal const val EXTRA_MAX_WIDTH = "extra.max_width" internal const val EXTRA_MAX_HEIGHT = "extra.max_height" + internal const val EXTRA_MULTIPLE_PICKER = "extra.multiple_picker" internal const val EXTRA_SAVE_DIRECTORY = "extra.save_directory" internal const val EXTRA_ERROR = "extra.error" @@ -117,6 +119,15 @@ open class ImagePicker { */ private var saveDir: String? = null + /** + * Multiple Picker + * + * Convert picker to multiple picker crop compress etc will be disabled in this case. + * + * If null, Image will be stored in "{fileDir}/Images" + */ + private var multiplePicker: Boolean = false + /** * Call this while picking image for fragment. */ @@ -259,6 +270,12 @@ open class ImagePicker { return this } + fun multiplePicker(multiplePicker: Boolean) :Builder { + this.multiplePicker = multiplePicker + return this + } + + /** * Start Image Picker Activity */ @@ -349,6 +366,8 @@ open class ImagePicker { putLong(EXTRA_IMAGE_MAX_SIZE, maxSize) putString(EXTRA_SAVE_DIRECTORY, saveDir) + + putBoolean(EXTRA_MULTIPLE_PICKER, multiplePicker) } } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerActivity.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerActivity.kt index 6f0e95bf..58382b0d 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerActivity.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/ImagePickerActivity.kt @@ -111,6 +111,11 @@ class ImagePickerActivity : AppCompatActivity() { mCropProvider.onActivityResult(requestCode, resultCode, data) } + override fun onDestroy() { + super.onDestroy() + mCompressionProvider.release() + } + /** * Handle Activity Back Press */ @@ -126,11 +131,20 @@ class ImagePickerActivity : AppCompatActivity() { fun setImage(uri: Uri) { when { mCropProvider.isCropEnabled() -> mCropProvider.startIntent(uri) - mCompressionProvider.isCompressionRequired(uri) -> mCompressionProvider.compress(uri) - else -> setResult(uri) + else -> mCompressionProvider.compressIfRequired(uri) } } + /** + * Multiples images captured, when multiples images are captured we will go to the result and, without cropping. + * + * @param uri Capture/Gallery image Uri + */ + fun setMultipleImages(uri: List) { + mCompressionProvider.compressIfRequired(uri) + } + + /** * {@link CropProviders} Result will be available here. * @@ -143,11 +157,7 @@ class ImagePickerActivity : AppCompatActivity() { // In case of Gallery Provider, we will get original image path, so we will not delete that. mCameraProvider?.delete() - if (mCompressionProvider.isCompressionRequired(uri)) { - mCompressionProvider.compress(uri) - } else { - setResult(uri) - } + mCompressionProvider.compressIfRequired(uri) } /** @@ -155,17 +165,19 @@ class ImagePickerActivity : AppCompatActivity() { * * @param uri Compressed image Uri */ - fun setCompressedImage(uri: Uri) { + fun setCompressedImage(uris: List) { // This is the case when Crop is not enabled // Delete Camera file after crop. Else there will be two image for the same action. // In case of Gallery Provider, we will get original image path, so we will not delete that. mCameraProvider?.delete() - // If crop file is not null, Delete it after crop - mCropProvider.delete() - - setResult(uri) + // Image is deleting without cause, I think this have a reason but in the new code formatter I can't found it. + // mCropProvider.delete() + if(uris.size == 1) + setResult(uris.first()) + else + setResult(uris) } /** @@ -181,6 +193,17 @@ class ImagePickerActivity : AppCompatActivity() { finish() } + /** + * Set Result, Multiple images picking is successfully picked/compressed. + * @param uris all uris image picked and compressed if compression are enabled. + */ + private fun setResult(uris: List) { + val intent = Intent() + intent.putExtra(ImagePicker.RESULT_MULTIPLE_FILES, uris.toTypedArray()) + setResult(Activity.RESULT_OK, intent) + finish() + } + /** * User has cancelled the task */ @@ -200,4 +223,5 @@ class ImagePickerActivity : AppCompatActivity() { setResult(ImagePicker.RESULT_ERROR, intent) finish() } + } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/exception/FailedToCompressException.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/exception/FailedToCompressException.kt new file mode 100644 index 00000000..193997f9 --- /dev/null +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/exception/FailedToCompressException.kt @@ -0,0 +1,4 @@ +package com.github.dhaval2404.imagepicker.exception + +class FailedToCompressException():Exception() { +} \ No newline at end of file diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CompressionProvider.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CompressionProvider.kt index 9555b73c..fc7a9be5 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CompressionProvider.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CompressionProvider.kt @@ -1,15 +1,17 @@ package com.github.dhaval2404.imagepicker.provider -import android.annotation.SuppressLint import android.graphics.Bitmap import android.net.Uri -import android.os.AsyncTask import android.os.Bundle +import androidx.annotation.WorkerThread import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePickerActivity +import com.github.dhaval2404.imagepicker.R +import com.github.dhaval2404.imagepicker.exception.FailedToCompressException import com.github.dhaval2404.imagepicker.util.ExifDataCopier import com.github.dhaval2404.imagepicker.util.FileUtil import com.github.dhaval2404.imagepicker.util.ImageUtil +import kotlinx.coroutines.* import java.io.File /** @@ -30,6 +32,7 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity private val mMaxFileSize: Long private val mFileDir: File + private val coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) init { val bundle = activity.intent.extras ?: Bundle() @@ -73,7 +76,7 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity * Check if compression is required * @param uri Uri object to apply Compression */ - fun isCompressionRequired(uri: Uri): Boolean { + private fun isCompressionRequired(uri: Uri): Boolean { val status = isCompressEnabled() && getSizeDiff(uri) > 0L if (!status && mMaxWidth > 0 && mMaxHeight > 0) { // Check image resolution @@ -93,42 +96,49 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity } /** - * Compress given file if enabled. + * Compress given file if enabled one or more. * * @param uri Uri to compress */ - fun compress(uri: Uri) { - startCompressionWorker(uri) + fun compressIfRequired(uris: List) { + coroutineScope.launch { + startCompressionWorker(uris) + this.cancel() + } + } + + fun compressIfRequired(uri: Uri) { + compressIfRequired(listOf(uri)) } /** - * Start Compression in Background + * Start Compression multiple or one files in Background */ - @SuppressLint("StaticFieldLeak") - private fun startCompressionWorker(uri: Uri) { - object : AsyncTask() { - override fun doInBackground(vararg params: Uri): File? { - // Perform operation in background - val file = FileUtil.getTempFile(this@CompressionProvider, params[0]) ?: return null - return startCompression(file) + @WorkerThread + private fun startCompressionWorker(uris: List) { + try { + val urisCompressed = uris.map { uriToCompress -> + if (isCompressionRequired(uriToCompress)) { + FileUtil.getTempFile(this@CompressionProvider, uriToCompress)?.let { + startCompression(it)?.let { file -> + Uri.fromFile(file) + } ?: throw FailedToCompressException() + } ?: throw FailedToCompressException() + } else uriToCompress } - override fun onPostExecute(file: File?) { - super.onPostExecute(file) - if (file != null) { - // Post Result - handleResult(file) - } else { - // Post Error - setError(com.github.dhaval2404.imagepicker.R.string.error_failed_to_compress_image) - } - } - }.execute(uri) + handleResult(urisCompressed) + } catch (e: Exception) { + e.printStackTrace() + setError(R.string.error_failed_to_compress_image) + } } /** * Check if compression required, And Apply compression until file size reach below Max Size. + * To be sure this function is only called from worker thread, added annotation worker thread. */ + @WorkerThread private fun startCompression(file: File): File? { var newFile: File? = null var attempt = 0 @@ -233,7 +243,13 @@ class CompressionProvider(activity: ImagePickerActivity) : BaseProvider(activity /** * This method will be called when final result fot this provider is enabled. */ - private fun handleResult(file: File) { - activity.setCompressedImage(Uri.fromFile(file)) + private fun handleResult(uri: List) { + activity.setCompressedImage(uri) + } + + fun release(){ + coroutineScope.cancel() } + + } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CropProvider.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CropProvider.kt index 826cbddf..2f55076c 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CropProvider.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/CropProvider.kt @@ -129,11 +129,11 @@ class CropProvider(activity: ImagePickerActivity) : BaseProvider(activity) { } catch (ex: ActivityNotFoundException) { setError( "uCrop not specified in manifest file." + - "Add UCropActivity in Manifest" + - "" + "Add UCropActivity in Manifest" + + "" ) ex.printStackTrace() } @@ -150,7 +150,8 @@ class CropProvider(activity: ImagePickerActivity) : BaseProvider(activity) { fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode == UCrop.REQUEST_CROP) { if (resultCode == Activity.RESULT_OK) { - handleResult(mCropImageFile) + val resultUri = UCrop.getOutput(data!!); + handleResult(resultUri) } else { setResultCancel() } @@ -162,9 +163,9 @@ class CropProvider(activity: ImagePickerActivity) : BaseProvider(activity) { * * @param file cropped file */ - private fun handleResult(file: File?) { - if (file != null) { - activity.setCropImage(Uri.fromFile(file)) + private fun handleResult(uri: Uri?) { + if (uri != null) { + activity.setCropImage(uri) } else { setError(R.string.error_failed_to_crop_image) } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/GalleryProvider.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/GalleryProvider.kt index 2451fcf9..83bb4092 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/GalleryProvider.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/provider/GalleryProvider.kt @@ -8,6 +8,8 @@ import com.github.dhaval2404.imagepicker.ImagePicker import com.github.dhaval2404.imagepicker.ImagePickerActivity import com.github.dhaval2404.imagepicker.R import com.github.dhaval2404.imagepicker.util.IntentUtils +import com.github.dhaval2404.imagepicker.util.forEach +import com.github.dhaval2404.imagepicker.util.getUris /** * Select image from Storage @@ -25,12 +27,14 @@ class GalleryProvider(activity: ImagePickerActivity) : // Mime types restrictions for gallery. By default all mime types are valid private val mimeTypes: Array + private val multiplePicker: Boolean init { val bundle = activity.intent.extras ?: Bundle() // Get MIME types mimeTypes = bundle.getStringArray(ImagePicker.EXTRA_MIME_TYPES) ?: emptyArray() + multiplePicker = bundle.getBoolean(ImagePicker.EXTRA_MULTIPLE_PICKER, false) } /** @@ -44,7 +48,7 @@ class GalleryProvider(activity: ImagePickerActivity) : * Start Gallery Intent */ private fun startGalleryIntent() { - val galleryIntent = IntentUtils.getGalleryIntent(activity, mimeTypes) + val galleryIntent = IntentUtils.getGalleryIntent(activity, mimeTypes, multiplePicker) activity.startActivityForResult(galleryIntent, GALLERY_INTENT_REQ_CODE) } @@ -70,11 +74,22 @@ class GalleryProvider(activity: ImagePickerActivity) : */ private fun handleResult(data: Intent?) { val uri = data?.data - if (uri != null) { - takePersistableUriPermission(uri) - activity.setImage(uri) - } else { - setError(R.string.error_failed_pick_gallery_image) + val clipData = data?.clipData + when { + uri != null -> { + takePersistableUriPermission(uri) + activity.setImage(uri) + } + clipData != null -> { + val uris = clipData.getUris().map { + takePersistableUriPermission(it) + it + } + activity.setMultipleImages(uris) + } + else -> { + setError(R.string.error_failed_pick_gallery_image) + } } } diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/ClipDataUtils.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/ClipDataUtils.kt new file mode 100644 index 00000000..3dad63ef --- /dev/null +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/ClipDataUtils.kt @@ -0,0 +1,20 @@ +package com.github.dhaval2404.imagepicker.util + +import android.content.ClipData +import android.net.Uri + + +fun ClipData.forEach(callback: (uri: Uri) -> Unit) { + for (i in 0 until this.itemCount) { + val uri: Uri = this.getItemAt(i).uri + callback(uri) + } +} + +fun ClipData.getUris():List { + val uris = mutableListOf() + for (i in 0 until this.itemCount) { + uris.add(this.getItemAt(i).uri) + } + return uris +} \ No newline at end of file diff --git a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/IntentUtils.kt b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/IntentUtils.kt index 5f75d833..7124d6f5 100644 --- a/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/IntentUtils.kt +++ b/imagepicker/src/main/kotlin/com/github/dhaval2404/imagepicker/util/IntentUtils.kt @@ -23,9 +23,13 @@ object IntentUtils { * @return Intent Gallery Intent */ @JvmStatic - fun getGalleryIntent(context: Context, mimeTypes: Array): Intent { + fun getGalleryIntent( + context: Context, + mimeTypes: Array, + multiplePicker: Boolean + ): Intent { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - val intent = getGalleryDocumentIntent(mimeTypes) + val intent = getGalleryDocumentIntent(mimeTypes, multiplePicker) if (intent.resolveActivity(context.packageManager) != null) { return intent } @@ -38,9 +42,14 @@ object IntentUtils { * * @return Intent Gallery Document Intent */ - private fun getGalleryDocumentIntent(mimeTypes: Array): Intent { + private fun getGalleryDocumentIntent( + mimeTypes: Array, + multiplePicker: Boolean + ): Intent { // Show Document Intent - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).applyImageTypes(mimeTypes) + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) + .applyImageTypes(mimeTypes) + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePicker) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) diff --git a/sample/src/main/kotlin/com.github.dhaval2404.imagepicker/sample/MainActivity.kt b/sample/src/main/kotlin/com.github.dhaval2404.imagepicker/sample/MainActivity.kt index 441a1732..f1170231 100644 --- a/sample/src/main/kotlin/com.github.dhaval2404.imagepicker/sample/MainActivity.kt +++ b/sample/src/main/kotlin/com.github.dhaval2404.imagepicker/sample/MainActivity.kt @@ -99,6 +99,7 @@ class MainActivity : AppCompatActivity() { @Suppress("UNUSED_PARAMETER") fun pickGalleryImage(view: View) { ImagePicker.with(this) + .multiplePicker(false) // Crop Image(User can choose Aspect Ratio) .crop() // User can only select image from Gallery @@ -161,7 +162,13 @@ class MainActivity : AppCompatActivity() { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) { // Uri object will not be null for RESULT_OK - val uri: Uri = data?.data!! + + val uri = if (data?.data != null) { + data.data!! + } else { + val images :List = data?.extras?.getParcelableArray(ImagePicker.RESULT_MULTIPLE_FILES)?.map { it as Uri }?.toList()!! + images[0] + } when (requestCode) { PROFILE_IMAGE_REQ_CODE -> { mProfileUri = uri