Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New feature multiple selection was added. #291

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions imagepicker/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -259,6 +270,12 @@ open class ImagePicker {
return this
}

fun multiplePicker(multiplePicker: Boolean) :Builder {
this.multiplePicker = multiplePicker
return this
}


/**
* Start Image Picker Activity
*/
Expand Down Expand Up @@ -349,6 +366,8 @@ open class ImagePicker {
putLong(EXTRA_IMAGE_MAX_SIZE, maxSize)

putString(EXTRA_SAVE_DIRECTORY, saveDir)

putBoolean(EXTRA_MULTIPLE_PICKER, multiplePicker)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,11 @@ class ImagePickerActivity : AppCompatActivity() {
mCropProvider.onActivityResult(requestCode, resultCode, data)
}

override fun onDestroy() {
super.onDestroy()
mCompressionProvider.release()
}

/**
* Handle Activity Back Press
*/
Expand All @@ -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<Uri>) {
mCompressionProvider.compressIfRequired(uri)
}


/**
* {@link CropProviders} Result will be available here.
*
Expand All @@ -143,29 +157,27 @@ 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)
}

/**
* {@link CompressionProvider} Result will be available here.
*
* @param uri Compressed image Uri
*/
fun setCompressedImage(uri: Uri) {
fun setCompressedImage(uris: List<Uri>) {
// 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)
}

/**
Expand All @@ -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<Uri>) {
val intent = Intent()
intent.putExtra(ImagePicker.RESULT_MULTIPLE_FILES, uris.toTypedArray())
setResult(Activity.RESULT_OK, intent)
finish()
}

/**
* User has cancelled the task
*/
Expand All @@ -200,4 +223,5 @@ class ImagePickerActivity : AppCompatActivity() {
setResult(ImagePicker.RESULT_ERROR, intent)
finish()
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.github.dhaval2404.imagepicker.exception

class FailedToCompressException():Exception() {
}
Original file line number Diff line number Diff line change
@@ -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

/**
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand All @@ -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<Uri>) {
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<Uri, Void, File>() {
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<Uri>) {
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
Expand Down Expand Up @@ -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<Uri>) {
activity.setCompressedImage(uri)
}

fun release(){
coroutineScope.cancel()
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ class CropProvider(activity: ImagePickerActivity) : BaseProvider(activity) {
} catch (ex: ActivityNotFoundException) {
setError(
"uCrop not specified in manifest file." +
"Add UCropActivity in Manifest" +
"<activity\n" +
" android:name=\"com.yalantis.ucrop.UCropActivity\"\n" +
" android:screenOrientation=\"portrait\"\n" +
" android:theme=\"@style/Theme.AppCompat.Light.NoActionBar\"/>"
"Add UCropActivity in Manifest" +
"<activity\n" +
" android:name=\"com.yalantis.ucrop.UCropActivity\"\n" +
" android:screenOrientation=\"portrait\"\n" +
" android:theme=\"@style/Theme.AppCompat.Light.NoActionBar\"/>"
)
ex.printStackTrace()
}
Expand All @@ -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()
}
Expand All @@ -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)
}
Expand Down
Loading