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