Skip to content

Commit

Permalink
Added intent chooser for selecting image sources (#326)
Browse files Browse the repository at this point in the history
* Added intent chooser for selecting image sources

* Made IntentChooser optional

* More acceptable title for intent chooser
  • Loading branch information
Devenom1 authored Apr 3, 2022
1 parent 829c8fa commit 657a9af
Show file tree
Hide file tree
Showing 6 changed files with 334 additions and 0 deletions.
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

0 comments on commit 657a9af

Please sign in to comment.