Skip to content

Commit 657a9af

Browse files
authored
Added intent chooser for selecting image sources (#326)
* Added intent chooser for selecting image sources * Made IntentChooser optional * More acceptable title for intent chooser
1 parent 829c8fa commit 657a9af

File tree

6 files changed

+334
-0
lines changed

6 files changed

+334
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1010
- `Security` in case of vulnerabilities.
1111

1212
## [x.x.x] - unreleased
13+
### Added
14+
- Added support for optionally displaying an intent chooser when selecting image source. [#325](https://github.com/CanHub/Android-Image-Cropper/issues/325)
1315
### Changed
1416
- CropException sealed class with cancellation and Image exceptions [#332](https://github.com/CanHub/Android-Image-Cropper/issues/332)
1517
### Fixed
1618
- Fix disable closing AlertDialog when touching outside the dialog [#334](https://github.com/CanHub/Android-Image-Cropper/issues/334)
19+
1720
## [4.2.0] - 21/03/2022
1821
### Added
1922
- Added an option to skip manual editing and return entire image when required [#324](https://github.com/CanHub/Android-Image-Cropper/pull/324)

cropper/src/main/java/com/canhub/cropper/CropImageActivity.kt

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ open class CropImageActivity :
6262
if (savedInstanceState == null) {
6363
if (cropImageUri == null || cropImageUri == Uri.EMPTY) {
6464
when {
65+
cropImageOptions.showIntentChooser -> showIntentChooser()
6566
cropImageOptions.imageSourceIncludeGallery &&
6667
cropImageOptions.imageSourceIncludeCamera ->
6768
showImageSourceDialog(::openSource)
@@ -86,6 +87,39 @@ open class CropImageActivity :
8687
}
8788
}
8889

90+
private fun showIntentChooser() {
91+
val ciIntentChooser = CropImageIntentChooser(
92+
activity = this,
93+
callback = object : CropImageIntentChooser.ResultCallback {
94+
override fun onSuccess(uri: Uri?) {
95+
onPickImageResult(uri)
96+
}
97+
98+
override fun onCancelled() {
99+
setResultCancel()
100+
}
101+
}
102+
)
103+
cropImageOptions.let { options ->
104+
options.intentChooserTitle
105+
?.takeIf { title -> title.isNotBlank() }
106+
?.let { icTitle ->
107+
ciIntentChooser.setIntentChooserTitle(icTitle)
108+
}
109+
options.intentChooserPriorityList
110+
?.takeIf { appPriorityList -> appPriorityList.isNotEmpty() }
111+
?.let { appsList ->
112+
ciIntentChooser.setupPriorityAppsList(appsList)
113+
}
114+
val cameraUri: Uri? = if (options.imageSourceIncludeCamera) getTmpFileUri() else null
115+
ciIntentChooser.showChooserIntent(
116+
includeCamera = options.imageSourceIncludeCamera,
117+
includeGallery = options.imageSourceIncludeGallery,
118+
cameraImgUri = cameraUri
119+
)
120+
}
121+
}
122+
89123
private fun openSource(source: Source) {
90124
when (source) {
91125
Source.CAMERA -> openCamera()
@@ -334,6 +368,7 @@ open class CropImageActivity :
334368
enum class Source { CAMERA, GALLERY }
335369

336370
private companion object {
371+
337372
const val BUNDLE_KEY_TMP_URI = "bundle_key_tmp_uri"
338373
}
339374
}

cropper/src/main/java/com/canhub/cropper/CropImageContractOptions.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ data class CropImageContractOptions @JvmOverloads constructor(
4949
cropImageOptions.cropShape = cropShape
5050
return this
5151
}
52+
5253
/**
5354
* To set the shape of the cropper corner (RECTANGLE / OVAL)
5455
* Default: RECTANGLE
@@ -500,6 +501,41 @@ data class CropImageContractOptions @JvmOverloads constructor(
500501
cropImageOptions.showCropOverlay = !skipEditing
501502
return this
502503
}
504+
505+
/**
506+
* Shows an intent chooser instead of the alert dialog when choosing an image source.
507+
*
508+
* *Default: false*
509+
*
510+
* Note: To show the camera app as an option in Intent chooser you will need to add
511+
* the camera permission ("android.permission.CAMERA") to your manifest file.
512+
*/
513+
fun setShowIntentChooser(showIntentChooser: Boolean) = cropImageOptions.apply {
514+
this.showIntentChooser = showIntentChooser
515+
}
516+
517+
/**
518+
* Sets a custom title for the intent chooser
519+
*/
520+
fun setIntentChooserTitle(intentChooserTitle: String) = cropImageOptions.apply {
521+
this.intentChooserTitle = intentChooserTitle
522+
}
523+
524+
/**
525+
* This takes the given app package list (list of app package names)
526+
* and displays them first among the list of apps available
527+
*
528+
* @param priorityAppPackages accepts a list of strings of app package names
529+
* Apps are displayed in the order you pass them if they are available on your device
530+
*
531+
* Note: If you pass an empty list here there will be no sorting of the apps list
532+
* shown in the intent chooser.
533+
* By default, the library sorts the list putting a few common
534+
* apps like Google Photos and Google Photos Go at the start of the list.
535+
*/
536+
fun setIntentChooserPriorityList(priorityAppPackages: List<String>) = cropImageOptions.apply {
537+
this.intentChooserPriorityList = priorityAppPackages
538+
}
503539
}
504540

505541
fun options(
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package com.canhub.cropper
2+
3+
import android.Manifest
4+
import android.app.Activity
5+
import android.content.ComponentName
6+
import android.content.Context
7+
import android.content.Intent
8+
import android.content.pm.PackageManager
9+
import android.net.Uri
10+
import android.os.Build
11+
import android.os.Parcelable
12+
import android.provider.MediaStore
13+
import androidx.activity.ComponentActivity
14+
import androidx.activity.result.contract.ActivityResultContracts
15+
16+
class CropImageIntentChooser(
17+
private val activity: ComponentActivity,
18+
private val callback: ResultCallback
19+
) {
20+
21+
interface ResultCallback {
22+
23+
fun onSuccess(uri: Uri?)
24+
25+
fun onCancelled()
26+
}
27+
28+
companion object {
29+
30+
const val GOOGLE_PHOTOS = "com.google.android.apps.photos"
31+
const val GOOGLE_PHOTOS_GO = "com.google.android.apps.photosgo"
32+
const val SAMSUNG_GALLERY = "com.sec.android.gallery3d"
33+
const val ONEPLUS_GALLERY = "com.oneplus.gallery"
34+
const val MIUI_GALLERY = "com.miui.gallery"
35+
}
36+
37+
private var title: String = activity.getString(R.string.pick_image_chooser_title)
38+
private var priorityIntentList = listOf(
39+
GOOGLE_PHOTOS,
40+
GOOGLE_PHOTOS_GO,
41+
SAMSUNG_GALLERY,
42+
ONEPLUS_GALLERY,
43+
MIUI_GALLERY
44+
)
45+
private var cameraImgUri: Uri? = null
46+
private val intentChooser =
47+
activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityRes ->
48+
if (activityRes.resultCode == Activity.RESULT_OK) {
49+
/*
50+
Here we don't know whether a gallery app or the camera app is selected
51+
via the intent chooser. If a gallery app is selected and an image is
52+
chosen then we get the result from activityRes.
53+
If a camera app is selected we take the uri we passed to the camera
54+
app for storing the captured image
55+
*/
56+
(activityRes.data?.data ?: cameraImgUri).let { uri ->
57+
callback.onSuccess(uri)
58+
}
59+
} else {
60+
callback.onCancelled()
61+
}
62+
}
63+
64+
/**
65+
* Create a chooser intent to select the source to get image from.<br></br>
66+
* The source can be camera's (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).<br></br>
67+
* All possible sources are added to the intent chooser.
68+
*
69+
* @param includeCamera if to include camera intents
70+
* @param includeGallery if to include Gallery app intents
71+
* @param cameraImgUri required if includeCamera is set to true
72+
*/
73+
fun showChooserIntent(
74+
includeCamera: Boolean,
75+
includeGallery: Boolean,
76+
cameraImgUri: Uri? = null
77+
) {
78+
this.cameraImgUri = cameraImgUri
79+
val allIntents: MutableList<Intent> = ArrayList()
80+
val packageManager = activity.packageManager
81+
// collect all camera intents if Camera permission is available
82+
if (!isExplicitCameraPermissionRequired(activity) && includeCamera) {
83+
allIntents.addAll(getCameraIntents(activity, packageManager))
84+
}
85+
if (includeGallery) {
86+
var galleryIntents = getGalleryIntents(packageManager, Intent.ACTION_GET_CONTENT)
87+
if (galleryIntents.isEmpty()) {
88+
// if no intents found for get-content try pick intent action (Huawei P9).
89+
galleryIntents = getGalleryIntents(packageManager, Intent.ACTION_PICK)
90+
}
91+
allIntents.addAll(galleryIntents)
92+
}
93+
val target = if (allIntents.isEmpty()) Intent() else {
94+
Intent(Intent.ACTION_CHOOSER, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
95+
if (includeGallery) {
96+
action = Intent.ACTION_PICK
97+
type = "image/*"
98+
}
99+
}
100+
}
101+
// Create a chooser from the main intent
102+
val chooserIntent = Intent.createChooser(target, title)
103+
// Add all other intents
104+
chooserIntent.putExtra(
105+
Intent.EXTRA_INITIAL_INTENTS, allIntents.toTypedArray<Parcelable>()
106+
)
107+
intentChooser.launch(chooserIntent)
108+
}
109+
110+
/**
111+
* Get all Camera intents for capturing image using device camera apps.
112+
*/
113+
private fun getCameraIntents(context: Context, packageManager: PackageManager): List<Intent> {
114+
val allIntents: MutableList<Intent> = ArrayList()
115+
// Determine Uri of camera image to save.
116+
val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
117+
val listCam = packageManager.queryIntentActivities(captureIntent, 0)
118+
for (resolveInfo in listCam) {
119+
val intent = Intent(captureIntent)
120+
intent.component = ComponentName(
121+
resolveInfo.activityInfo.packageName,
122+
resolveInfo.activityInfo.name
123+
)
124+
intent.setPackage(resolveInfo.activityInfo.packageName)
125+
if (context is Activity) {
126+
context.grantUriPermission(
127+
resolveInfo.activityInfo.packageName, cameraImgUri,
128+
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
129+
)
130+
}
131+
intent.putExtra(MediaStore.EXTRA_OUTPUT, cameraImgUri)
132+
allIntents.add(intent)
133+
}
134+
return allIntents
135+
}
136+
137+
/**
138+
* Get all Gallery intents for getting image from one of the apps of the device that handle
139+
* images.
140+
* Note: It currently get only the main camera app intent. Still have to figure out
141+
* how to get multiple camera apps to pick from (if available)
142+
*/
143+
private fun getGalleryIntents(packageManager: PackageManager, action: String): List<Intent> {
144+
val intents: MutableList<Intent> = ArrayList()
145+
val galleryIntent = if (action == Intent.ACTION_GET_CONTENT) Intent(action)
146+
else Intent(action, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
147+
galleryIntent.type = "image/*"
148+
val listGallery = packageManager.queryIntentActivities(galleryIntent, 0)
149+
for (res in listGallery) {
150+
val intent = Intent(galleryIntent)
151+
intent.component = ComponentName(res.activityInfo.packageName, res.activityInfo.name)
152+
intent.setPackage(res.activityInfo.packageName)
153+
intents.add(intent)
154+
}
155+
// sort intents
156+
val priorityIntents = mutableListOf<Intent>()
157+
for (pkgName in priorityIntentList) {
158+
intents.firstOrNull { it.`package` == pkgName }?.let {
159+
intents.remove(it)
160+
priorityIntents.add(it)
161+
}
162+
}
163+
intents.addAll(0, priorityIntents)
164+
return intents
165+
}
166+
167+
/**
168+
* Check if explicetly requesting camera permission is required.<br></br>
169+
* It is required in Android Marshmellow and above if "CAMERA" permission is requested in the
170+
* manifest.<br></br>
171+
* See [StackOverflow
172+
* question](http://stackoverflow.com/questions/32789027/android-m-camera-intent-permission-bug).
173+
*/
174+
private fun isExplicitCameraPermissionRequired(context: Context): Boolean {
175+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
176+
hasCameraPermissionInManifest(context) &&
177+
context.checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
178+
}
179+
180+
/**
181+
* Check if the app requests a specific permission in the manifest.
182+
*
183+
* @param context the context of your activity to check for permissions
184+
* @return true - the permission in requested in manifest, false - not.
185+
*/
186+
private fun hasCameraPermissionInManifest(context: Context): Boolean {
187+
val packageName = context.packageName
188+
try {
189+
val packageInfo =
190+
context.packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
191+
val declaredPermissions = packageInfo.requestedPermissions
192+
return declaredPermissions
193+
?.any { it?.equals("android.permission.CAMERA", true) == true } == true
194+
} catch (e: PackageManager.NameNotFoundException) {
195+
// Since the package name cannot be found we return false below
196+
// because this means that the camera permission hasn't been declared
197+
// by the user for this package so we can't show the camera app among
198+
// among the list of apps
199+
e.printStackTrace()
200+
}
201+
return false
202+
}
203+
204+
/**
205+
* Set up a list of apps that you require to show first in the intent chooser
206+
* Apps will show in the order it is passed
207+
*
208+
* @param appsList - pass a list of package names of apps of your choice
209+
*
210+
* This overrides the existing apps list
211+
*/
212+
fun setupPriorityAppsList(appsList: List<String>): CropImageIntentChooser = apply {
213+
priorityIntentList = appsList
214+
}
215+
216+
/**
217+
* Set the title for the intent chooser
218+
*
219+
* @param title - the title for the intent chooser
220+
*/
221+
fun setIntentChooserTitle(title: String): CropImageIntentChooser = apply {
222+
this.title = title
223+
}
224+
}

0 commit comments

Comments
 (0)