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 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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ 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)
### Changed
- CropException sealed class with cancellation and Image exceptions [#332](https://github.com/CanHub/Android-Image-Cropper/issues/332)
### Fixed
- Fix disable closing AlertDialog when touching outside the dialog [#334](https://github.com/CanHub/Android-Image-Cropper/issues/334)

## [4.2.0] - 21/03/2022
### Added
- Added an option to skip manual editing and return entire image when required [#324](https://github.com/CanHub/Android-Image-Cropper/pull/324)
Expand Down
35 changes: 35 additions & 0 deletions cropper/src/main/java/com/canhub/cropper/CropImageActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ open class CropImageActivity :
if (savedInstanceState == null) {
if (cropImageUri == null || cropImageUri == Uri.EMPTY) {
when {
cropImageOptions.showIntentChooser -> showIntentChooser()
cropImageOptions.imageSourceIncludeGallery &&
cropImageOptions.imageSourceIncludeCamera ->
showImageSourceDialog(::openSource)
Expand All @@ -86,6 +87,39 @@ open class CropImageActivity :
}
}

private fun showIntentChooser() {
val ciIntentChooser = CropImageIntentChooser(
activity = this,
callback = object : CropImageIntentChooser.ResultCallback {
override fun onSuccess(uri: Uri?) {
onPickImageResult(uri)
}

override fun onCancelled() {
setResultCancel()
}
}
)
cropImageOptions.let { options ->
options.intentChooserTitle
?.takeIf { title -> title.isNotBlank() }
?.let { icTitle ->
ciIntentChooser.setIntentChooserTitle(icTitle)
}
options.intentChooserPriorityList
?.takeIf { appPriorityList -> appPriorityList.isNotEmpty() }
?.let { appsList ->
ciIntentChooser.setupPriorityAppsList(appsList)
}
val cameraUri: Uri? = if (options.imageSourceIncludeCamera) getTmpFileUri() else null
ciIntentChooser.showChooserIntent(
includeCamera = options.imageSourceIncludeCamera,
includeGallery = options.imageSourceIncludeGallery,
cameraImgUri = cameraUri
)
}
}

private fun openSource(source: Source) {
when (source) {
Source.CAMERA -> openCamera()
Expand Down Expand Up @@ -334,6 +368,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,41 @@ 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*
*
* Note: To show the camera app as an option in Intent chooser you will need to add
* the camera permission ("android.permission.CAMERA") to your manifest file.
*/
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
*
* Note: If you pass an empty list here there will be no sorting of the apps list
* shown in the intent chooser.
* By default, the library sorts the list putting a few common
* apps like Google Photos and Google Photos Go at the start of the list.
*/
fun setIntentChooserPriorityList(priorityAppPackages: List<String>) = cropImageOptions.apply {
this.intentChooserPriorityList = priorityAppPackages
}
}

fun options(
Expand Down
224 changes: 224 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,224 @@
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
) {

interface ResultCallback {

fun onSuccess(uri: Uri?)

fun onCancelled()
}

companion object {

const val GOOGLE_PHOTOS = "com.google.android.apps.photos"
const val GOOGLE_PHOTOS_GO = "com.google.android.apps.photosgo"
const val SAMSUNG_GALLERY = "com.sec.android.gallery3d"
const val ONEPLUS_GALLERY = "com.oneplus.gallery"
const val MIUI_GALLERY = "com.miui.gallery"
}

private var title: String = activity.getString(R.string.pick_image_chooser_title)
private var priorityIntentList = listOf(
GOOGLE_PHOTOS,
GOOGLE_PHOTOS_GO,
SAMSUNG_GALLERY,
ONEPLUS_GALLERY,
MIUI_GALLERY
)
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 = if (allIntents.isEmpty()) Intent() else {
Intent(Intent.ACTION_CHOOSER, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
if (includeGallery) {
action = Intent.ACTION_PICK
type = "image/*"
}
}
}
// Create a chooser from the main intent
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
return declaredPermissions
?.any { it?.equals("android.permission.CAMERA", true) == true } == true
} 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
}

/**
* 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
}
}
Loading