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

Added intent chooser for selecting image sources #326

Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- `Security` in case of vulnerabilities.

## [x.x.x] - unreleased
### Added
- Added support for optionally displaying an intent chooser when selecting image source. [#325](https://github.com/CanHub/Android-Image-Cropper/issues/325)

## [4.2.0] - 21/03/2022
### Added
Expand Down
47 changes: 39 additions & 8 deletions cropper/src/main/java/com/canhub/cropper/CropImageActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,45 @@ open class CropImageActivity :

if (savedInstanceState == null) {
if (cropImageUri == null || cropImageUri == Uri.EMPTY) {
when {
cropImageOptions.imageSourceIncludeGallery &&
if (cropImageOptions.showIntentChooser) {
Canato marked this conversation as resolved.
Show resolved Hide resolved
val ciIntentChooser = CropImageIntentChooser(
activity = this,
callback = object : CropImageIntentChooser.ResultCallback {
override fun onSuccess(uri: Uri?) {
onPickImageResult(uri)
}

override fun onCancelled() {
setResultCancel()
}
}
)
cropImageOptions.apply {
Canato marked this conversation as resolved.
Show resolved Hide resolved
intentChooserTitle?.takeIf { it.isNotBlank() }?.let { icTitle ->
ciIntentChooser.setIntentChooserTitle(icTitle)
}
intentChooserPriorityList?.takeIf { it.isNotEmpty() }?.let { appsList ->
Canato marked this conversation as resolved.
Show resolved Hide resolved
ciIntentChooser.setupPriorityAppsList(appsList)
}
val cameraUri: Uri? = if (imageSourceIncludeCamera) getTmpFileUri()
else null
ciIntentChooser.showChooserIntent(
includeCamera = imageSourceIncludeCamera,
includeGallery = imageSourceIncludeGallery,
cameraUri
)
}
Canato marked this conversation as resolved.
Show resolved Hide resolved
} else {
when {
cropImageOptions.imageSourceIncludeGallery &&
cropImageOptions.imageSourceIncludeCamera ->
showImageSourceDialog(::openSource)
cropImageOptions.imageSourceIncludeGallery ->
pickImageGallery.launch("image/*")
cropImageOptions.imageSourceIncludeCamera ->
showImageSourceDialog(::openSource)
cropImageOptions.imageSourceIncludeGallery ->
pickImageGallery.launch("image/*")
cropImageOptions.imageSourceIncludeCamera ->
openCamera()
else -> finish()
openCamera()
else -> finish()
}
}
} else cropImageView?.setImageUriAsync(cropImageUri)
} else {
Expand Down Expand Up @@ -333,6 +363,7 @@ open class CropImageActivity :
enum class Source { CAMERA, GALLERY }

private companion object {

const val BUNDLE_KEY_TMP_URI = "bundle_key_tmp_uri"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ data class CropImageContractOptions @JvmOverloads constructor(
cropImageOptions.cropShape = cropShape
return this
}

/**
* To set the shape of the cropper corner (RECTANGLE / OVAL)
* Default: RECTANGLE
Expand Down Expand Up @@ -500,6 +501,32 @@ data class CropImageContractOptions @JvmOverloads constructor(
cropImageOptions.showCropOverlay = !skipEditing
return this
}

/**
* Shows an intent chooser instead of the alert dialog when choosing an image source.
* *Default: false*
*/
fun setShowIntentChooser(showIntentChooser: Boolean) = cropImageOptions.apply {
this.showIntentChooser = showIntentChooser
}

/**
* Sets a custom title for the intent chooser
*/
fun setIntentChooserTitle(intentChooserTitle: String) = cropImageOptions.apply {
this.intentChooserTitle = intentChooserTitle
}

/**
* This takes the given app package list (list of app package names)
* and displays them first among the list of apps available
*
* @param priorityAppPackages accepts a list of strings of app package names
* Apps are displayed in the order you pass them if they are available on your device
*/
fun setIntentChooserPriorityList(priorityAppPackages: List<String>) = cropImageOptions.apply {
this.intentChooserPriorityList = priorityAppPackages
}
Canato marked this conversation as resolved.
Show resolved Hide resolved
}

fun options(
Expand Down
223 changes: 223 additions & 0 deletions cropper/src/main/java/com/canhub/cropper/CropImageIntentChooser.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package com.canhub.cropper

import android.Manifest
import android.app.Activity
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Parcelable
import android.provider.MediaStore
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts

class CropImageIntentChooser(
private val activity: ComponentActivity,
private val callback: ResultCallback
) {

private var title: String = activity.getString(R.string.pick_image_chooser_title)
private var priorityIntentList = listOf(
"com.google.android.apps.photos", // Google Photos
"com.google.android.apps.photosgo", // Google Photos Gallery Go
"com.sec.android.gallery3d", // Samsung Gallery
"com.oneplus.gallery", // One Plus Gallery
"com.miui.gallery", // MIUI Gallery
Canato marked this conversation as resolved.
Show resolved Hide resolved
)
private var cameraImgUri: Uri? = null
private val intentChooser =
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityRes ->
if (activityRes.resultCode == Activity.RESULT_OK) {
/*
Here we don't know whether a gallery app or the camera app is selected
via the intent chooser. If a gallery app is selected and an image is
chosen then we get the result from activityRes.
If a camera app is selected we take the uri we passed to the camera
app for storing the captured image
*/
(activityRes.data?.data ?: cameraImgUri).let { uri ->
callback.onSuccess(uri)
}
} else {
callback.onCancelled()
}
}

/**
* Create a chooser intent to select the source to get image from.<br></br>
* The source can be camera's (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).<br></br>
* All possible sources are added to the intent chooser.
*
* @param includeCamera if to include camera intents
* @param includeGallery if to include Gallery app intents
* @param cameraImgUri required if includeCamera is set to true
*/
fun showChooserIntent(
includeCamera: Boolean,
includeGallery: Boolean,
cameraImgUri: Uri? = null
) {
this.cameraImgUri = cameraImgUri
val allIntents: MutableList<Intent> = ArrayList()
val packageManager = activity.packageManager
// collect all camera intents if Camera permission is available
if (!isExplicitCameraPermissionRequired(activity) && includeCamera) {
allIntents.addAll(getCameraIntents(activity, packageManager))
}
if (includeGallery) {
var galleryIntents = getGalleryIntents(packageManager, Intent.ACTION_GET_CONTENT)
if (galleryIntents.isEmpty()) {
// if no intents found for get-content try pick intent action (Huawei P9).
galleryIntents = getGalleryIntents(packageManager, Intent.ACTION_PICK)
}
allIntents.addAll(galleryIntents)
}
val target: Intent
if (allIntents.isEmpty()) {
target = Intent()
} else {
target = Intent(Intent.ACTION_CHOOSER, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
if (includeGallery) {
target.action = Intent.ACTION_PICK
target.type = "image/*"
}
}
Canato marked this conversation as resolved.
Show resolved Hide resolved
// Create a chooser from the main intent
Canato marked this conversation as resolved.
Show resolved Hide resolved
val chooserIntent = Intent.createChooser(target, title)
// Add all other intents
chooserIntent.putExtra(
Intent.EXTRA_INITIAL_INTENTS, allIntents.toTypedArray<Parcelable>()
)
intentChooser.launch(chooserIntent)
}

/**
* Get all Camera intents for capturing image using device camera apps.
*/
private fun getCameraIntents(context: Context, packageManager: PackageManager): List<Intent> {
val allIntents: MutableList<Intent> = ArrayList()
// Determine Uri of camera image to save.
val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val listCam = packageManager.queryIntentActivities(captureIntent, 0)
for (resolveInfo in listCam) {
val intent = Intent(captureIntent)
intent.component = ComponentName(
resolveInfo.activityInfo.packageName,
resolveInfo.activityInfo.name
)
intent.setPackage(resolveInfo.activityInfo.packageName)
if (context is Activity) {
context.grantUriPermission(
resolveInfo.activityInfo.packageName, cameraImgUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
)
}
intent.putExtra(MediaStore.EXTRA_OUTPUT, cameraImgUri)
allIntents.add(intent)
}
return allIntents
}

/**
* Get all Gallery intents for getting image from one of the apps of the device that handle
* images.
* Note: It currently get only the main camera app intent. Still have to figure out
* how to get multiple camera apps to pick from (if available)
*/
private fun getGalleryIntents(packageManager: PackageManager, action: String): List<Intent> {
val intents: MutableList<Intent> = ArrayList()
val galleryIntent = if (action == Intent.ACTION_GET_CONTENT) Intent(action)
else Intent(action, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
galleryIntent.type = "image/*"
val listGallery = packageManager.queryIntentActivities(galleryIntent, 0)
for (res in listGallery) {
val intent = Intent(galleryIntent)
intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
intent.setPackage(res.activityInfo.packageName)
intents.add(intent)
}
// sort intents
val priorityIntents = mutableListOf<Intent>()
for (pkgName in priorityIntentList) {
intents.firstOrNull { it.`package` == pkgName }?.let {
intents.remove(it)
priorityIntents.add(it)
}
}
intents.addAll(0, priorityIntents)
return intents
}

/**
* Check if explicetly requesting camera permission is required.<br></br>
* It is required in Android Marshmellow and above if "CAMERA" permission is requested in the
* manifest.<br></br>
* See [StackOverflow
* question](http://stackoverflow.com/questions/32789027/android-m-camera-intent-permission-bug).
*/
private fun isExplicitCameraPermissionRequired(context: Context): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
hasCameraPermissionInManifest(context) &&
context.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
}

/**
* Check if the app requests a specific permission in the manifest.
*
* @param context the context of your activity to check for permissions
* @return true - the permission in requested in manifest, false - not.
*/
private fun hasCameraPermissionInManifest(context: Context): Boolean {
val packageName = context.packageName
try {
val packageInfo =
context.packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
val declaredPermissions = packageInfo.requestedPermissions
if (
declaredPermissions
?.any {
it?.equals("android.permission.CAMERA", true) == true
} == true
) {
return true
}
Canato marked this conversation as resolved.
Show resolved Hide resolved
} catch (e: PackageManager.NameNotFoundException) {
// Since the package name cannot be found we return false below
// because this means that the camera permission hasn't been declared
// by the user for this package so we can't show the camera app among
// among the list of apps
e.printStackTrace()
}
return false
}
Canato marked this conversation as resolved.
Show resolved Hide resolved

/**
* Set up a list of apps that you require to show first in the intent chooser
* Apps will show in the order it is passed
*
* @param appsList - pass a list of package names of apps of your choice
*
* This overrides the existing apps list
*/
fun setupPriorityAppsList(appsList: List<String>): CropImageIntentChooser = apply {
priorityIntentList = appsList
}

/**
* Set the title for the intent chooser
*
* @param title - the title for the intent chooser
*/
fun setIntentChooserTitle(title: String): CropImageIntentChooser = apply {
this.title = title
}

interface ResultCallback {

fun onSuccess(uri: Uri?)

fun onCancelled()
}
Canato marked this conversation as resolved.
Show resolved Hide resolved
}
29 changes: 29 additions & 0 deletions cropper/src/main/java/com/canhub/cropper/CropImageOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,26 @@ open class CropImageOptions : Parcelable {
@JvmField
var skipEditing: Boolean

/**
* Enabling this option replaces the current AlertDialog to choose the image source
* with an Intent chooser
*/
@JvmField
var showIntentChooser: Boolean

/**
* optional, Sets a custom title for the intent chooser
*/
@JvmField
var intentChooserTitle: String?

/**
* optional, reorders intent list displayed with the app package names
* passed here in order
*/
@JvmField
var intentChooserPriorityList: List<String>?

/** Init options with defaults. */
constructor() {
val dm = Resources.getSystem().displayMetrics
Expand Down Expand Up @@ -350,6 +370,9 @@ open class CropImageOptions : Parcelable {
cropMenuCropButtonTitle = null
cropMenuCropButtonIcon = 0
skipEditing = false
showIntentChooser = false
intentChooserTitle = null
intentChooserPriorityList = listOf()
}

/** Create object from parcel. */
Expand Down Expand Up @@ -409,6 +432,9 @@ open class CropImageOptions : Parcelable {
cropMenuCropButtonTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel)
cropMenuCropButtonIcon = parcel.readInt()
skipEditing = parcel.readByte().toInt() != 0
showIntentChooser = parcel.readByte().toInt() != 0
intentChooserTitle = parcel.readString()
intentChooserPriorityList = parcel.createStringArrayList()
}

override fun writeToParcel(dest: Parcel, flags: Int) {
Expand Down Expand Up @@ -467,6 +493,9 @@ open class CropImageOptions : Parcelable {
TextUtils.writeToParcel(cropMenuCropButtonTitle, dest, flags)
dest.writeInt(cropMenuCropButtonIcon)
dest.writeByte((if (skipEditing) 1 else 0).toByte())
dest.writeByte((if (showIntentChooser) 1 else 0).toByte())
dest.writeString(intentChooserTitle)
dest.writeStringList(intentChooserPriorityList)
}

override fun describeContents(): Int {
Expand Down
Loading