Open adfwhitestar opened 3 years ago
Yes, same issue on my Pixel 3XL device after upgrade to Android 11
Same issue being reported by hundreds of users of our apps. Anyone have a workaround for Android 11?
Same. I downgraded the my targetSdkVersion to 29 instead of 30 and the image doesn't update.
Hi i've noted this issue on my Pixel 3a as well running Android 11
minSdkVersion 23 targetSdkVersion 29
I think this library needs to support a behaviour change outlined here, https://developer.android.com/about/versions/11/behavior-changes-all#share-content-uris
In our app, we have minSdk as 21 and target as 28. We're updating to 29 soon (since we're being forced by the Googles) but just wanted to chime in to say that it's happening for apps targeting 28 for devices running Android 11.
If anyone is able to fork the library and implement this and post a link here, I'm sure many would be grateful. This library hasn't been updated in years, and it's a shame because the alternatives out there are not good. I know I tried to move away from this library a year ago and tried 5+ other similar libraries and none of them were even close to as good as this one.
A while back I switched to use the fork here: https://github.com/ArthurHub/Android-Image-Cropper/pull/736 in order to support picking from the Gallery consistently across all devices. This is probably an important change to include if anyone is able to update this library, as without it, selecting from Gallery does not work on all devices.
I have the same, sometimes it works sometimes it doesn't Pixel4 Android11
Instead of CropImage.startPickImageActivity(this)
use:
fun pickPhoto() {
val documentsIntent = Intent(Intent.ACTION_GET_CONTENT)
documentsIntent.type = "image/*"
val otherGalleriesIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
otherGalleriesIntent.type = "image/*"
val chooserIntent = Intent.createChooser(
documentsIntent,
getString(R.string.pick_image_intent_chooser_title)
).putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(otherGalleriesIntent))
startActivityForResult(
chooserIntent,
CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE
)
}
Instead of
CropImage.startPickImageActivity(this)
use:fun pickPhoto() { val documentsIntent = Intent(Intent.ACTION_GET_CONTENT) documentsIntent.type = "image/*" val otherGalleriesIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI) otherGalleriesIntent.type = "image/*" val chooserIntent = Intent.createChooser( documentsIntent, getString(R.string.pick_image_intent_chooser_title) ).putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(otherGalleriesIntent)) startActivityForResult( chooserIntent, CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE ) }
the Only Issue I see with this is that it bypasses the Crop Activity and come back to the app. So you need to add another step to send the data back to CropImage so then it can send something back.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
when(requestCode){
CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE -> {
val uri = data?.data
//this is written from a fragment.
CropImage.activity(uri).start(requireContext(),this@yourFragment)
}
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE->{
val uri = (CropImage.getActivityResult(data) as CropImage.ActivityResult).uri
//do something with your UI
}
}
}
}
This code is also missing Cameras in the intent as written.
I got photo capture to work somewhat on Android 11. I followed the official guide at https://developer.android.com/training/camera/photobasics but had to make some adjustments.
The root of the issue seems to be that on Android 11 you must use FileProvider.getUriForFile() to generate the photo file URI that is passed to the camera app. Android-Image-Cropper uses Uri.fromFile(). If you have Android-Image-Cropper on your project, seems like you cannot create a FileProvider by yourself since the library already includes one. So as a workaround you have to accommodate to using the library-provided FileProvider.
The following code works for me on Android 11 emulator, run in a Fragment. It opens the camera directly and after returning in onActivityResult you can pass photoUri to CropImage.activity(photoUri) to crop it.
val REQUEST_TAKE_PHOTO = 112
var photoURI: Uri? = null
private fun dispatchTakePictureIntent() {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
// Ensure that there's a camera activity to handle the intent
takePictureIntent.resolveActivity(requireActivity().packageManager)?.also {
// Create the File where the photo should go
val getImage = requireContext().cacheDir ?: return
val photoFile: File? = try {
File(getImage.path, "pickImageResult.jpeg")
} catch (ex: IOException) {
// Error occurred while creating the File
null
}
// Continue only if the File was successfully created
photoFile?.also {
photoURI = FileProvider.getUriForFile(
requireActivity(),
requireContext().packageName + ".provider",
it
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
startActivityForResult(takePictureIntent, REQUEST_TAKE_PHOTO)
}
}
}
}
You also have to add this query in AndroidManifest.xml inside the manifest section:
<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
</queries>
Now, it would be awesome to make this change in the library directly. I think CropImage.getCaptureImageOutputUri should be updated to use FileProvider on newer Android versions. But I don't have the time to start testing this right now, gotta quickfix problems in my released apps before that 😅
I think, as @korva pointed this issue relates to changes of package visibility. I've fixed it for my project by adding queries
part into manifest. More info
I think, as @korva pointed this issue relates to changes of package visibility. I've fixed it for my project by adding
queries
part into manifest. More info
What did you add to your manifest?
@ajackson2907 In my case I added this:
<queries>
<intent>
<action android:name="android.intent.action.GET_CONTENT"/>
<data android:mimeType="image/*"/>
</intent>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE"/>
</intent>
</queries>
First for gallery type apps to select image. Second for camera apps.
Intents I create in this way:
// select image on the phone
Intent(Intent.ACTION_PICK).apply {
type = "image/*"
action = Intent.ACTION_GET_CONTENT
putExtra(Intent.EXTRA_LOCAL_ONLY, true)
}
// use camera to get image
// file is file in app directory to not request WRITE_EXTERNAL_STORAGE permission
// photoOutput is Uri. For Android N and above it is FileProvider.getUriForFile and app has it's own file provider.
// For Android below N it is just Uri.fromFile(file)
Intent(MediaStore.ACTION_IMAGE_CAPTURE)
.putExtra("PHOTO_IMAGE_PATH", file.absolutePath)
.putExtra(MediaStore.EXTRA_OUTPUT, photoOutput)
@ajackson2907 In my case I added this:
<queries> <intent> <action android:name="android.intent.action.GET_CONTENT"/> <data android:mimeType="image/*"/> </intent> <intent> <action android:name="android.media.action.IMAGE_CAPTURE"/> </intent> </queries>
First for gallery type apps to select image. Second for camera apps.
Intents I create in this way:
// select image on the phone Intent(Intent.ACTION_PICK).apply { type = "image/*" action = Intent.ACTION_GET_CONTENT putExtra(Intent.EXTRA_LOCAL_ONLY, true) }
// use camera to get image // file is file in app directory to not request WRITE_EXTERNAL_STORAGE permission // photoOutput is Uri. For Android N and above it is FileProvider.getUriForFile and app has it's own file provider. // For Android below N it is just Uri.fromFile(file) Intent(MediaStore.ACTION_IMAGE_CAPTURE) .putExtra("PHOTO_IMAGE_PATH", file.absolutePath) .putExtra(MediaStore.EXTRA_OUTPUT, photoOutput)
@fAntel Thanks for the reply, I will give it a go.
@ajackson2907 and others in this thread - do you have any suggestions for alternate libraries that do the same thing as this one? I know I tried many out a few years ago and this was the best at the time. I'd love to switch our apps to a library that's still being maintained instead of putting band aids on this one.
Thanks in advance for anyone that has suggestions for migrating away from this dead library.
@ajackson2907 and others in this thread - do you have any suggestions for alternate libraries that do the same thing as this one? I know I tried many out a few years ago and this was the best at the time. I'd love to switch our apps to a library that's still being maintained instead of putting band aids on this one.
Thanks in advance for anyone that has suggestions for migrating away from this dead library.
@matthewkrueger Not many that give you the choice of camera or gallery, could be easier for you to fetch your own image and just use it as a crop library, here is a list, not tried any of them though or checked if they are still being maintained.
https://ourcodeworld.com/articles/read/930/top-10-best-android-image-cropping-crop-widget-libraries
Putting together all the bits and pieces in this thread to hopefully help others, here's an abstract Fragment you can subclass to make this all work on Android 11. If you don't want to use the Anko custom view dialog stuff, substitute with your own dialog to choose a photo from camera or gallery. You may also need to add the <queries>
section to your manifest, as detailed above.
Call selectOrTakePhoto()
in your Fragment subclass, and the user will be prompted to take a photo, or select one from their Gallery app. After the photo is taken/selected, it gets passed on to CropImage.activity(uri) for cropping. Once you finish cropping, updateImage(File)
is called to pass the image file back to the calling Fragment, and you can update your UI to show the file from disk.
Enjoy!
Note: our app styles the primary/cancel buttons on the dialog, but I removed that from this code. This is just an example of what it looks like in our app.
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.provider.MediaStore
import android.text.format.DateFormat
import android.view.View
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import com.theartofdev.edmodo.cropper.CropImage
import com.theartofdev.edmodo.cropper.CropImageView
import timber.log.Timber
import org.jetbrains.anko.customView
import org.jetbrains.anko.include
import org.jetbrains.anko.support.v4.alert
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.*
/**
* Created by Matthew Krueger on 11/12/20.
*/
abstract class PhotoSelectorFragment: Fragment() {
abstract fun updateImage(imageFile: File?)
val imagePrefix = "Person"
var photoUri: Uri? = null
fun selectOrTakePhoto() {
if (hasRuntimePermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, PhotoSelector.RESULT_STORAGE_PERMISSION)) {
promptToTakeOrSelectPhoto(requireContext())
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
handleActivityResult(requireActivity(), requestCode, resultCode, data)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
handleRequestPermissionsResult(requestCode, permissions, grantResults)
}
/**
* Prompt to take or select then crop a photo.
* @param context
* @param fragment
*/
fun promptToTakeOrSelectPhoto(context: Context) {
var dialog: DialogInterface? = null
dialog = alert {
customView {
include<View>(R.layout.dialog_simple_yes_no) {
this.dialog_title.text = context.getString(R.string.profile_choose_photo)
this.dialog_message.visibility = View.GONE
this.dialog_yes_btn.setText(R.string.all_camera)
this.dialog_no_btn.setText(R.string.all_gallery)
this.dialog_yes_btn.setOnClickListener {
dialog?.dismiss()
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
// Ensure that there's a camera activity to handle the intent
takePictureIntent.resolveActivity(context.packageManager)?.also {
// Create the File where the photo should go
val getImage = context.cacheDir ?: return@setOnClickListener
val photoFile: File? = try {
File(getImage.path, "pickImageResult.jpeg")
} catch (ex: IOException) {
// Error occurred while creating the File
null
}
// Continue only if the File was successfully created
photoFile?.also {
photoUri = FileProvider.getUriForFile(
context,
context.packageName + ".fileProvider",
it
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
startActivityForResult(takePictureIntent, TAKE_PHOTO_REQUEST)
}
}
}
}
this.dialog_no_btn.setOnClickListener {
dialog?.dismiss()
// Pick an image from the gallery.
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
startActivityForResult(intent, PICK_PHOTO_REQUEST_CODE)
}
}
}
}.show()
}
fun handleActivityResult(context: Context, requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK && requestCode == PICK_PHOTO_REQUEST_CODE) {
val uri = data?.data
// Crop the image picked by the user.
CropImage.activity(uri)
.setGuidelines(CropImageView.Guidelines.ON_TOUCH)
.setCropShape(CropImageView.CropShape.OVAL)
.setFixAspectRatio(true)
.setScaleType(CropImageView.ScaleType.CENTER_INSIDE)
.setRequestedSize(PhotoSelectorDialog.PHOTO_SIZE_PX, PhotoSelectorDialog.PHOTO_SIZE_PX, CropImageView.RequestSizeOptions.RESIZE_EXACT)
.setActivityTitle(context.getString(R.string.profile_choose_photo))
.setAllowFlipping(false)
.start(context, this)
}
else if (resultCode == Activity.RESULT_OK && requestCode == TAKE_PHOTO_REQUEST) {
if (photoUri == null) {
Timber.w("photoUri is null, don't try to crop.")
}
// Crop the image taken by the user.
CropImage.activity(photoUri)
.setGuidelines(CropImageView.Guidelines.ON_TOUCH)
.setCropShape(CropImageView.CropShape.OVAL)
.setFixAspectRatio(true)
.setScaleType(CropImageView.ScaleType.CENTER_INSIDE)
.setRequestedSize(PhotoSelectorDialog.PHOTO_SIZE_PX, PhotoSelectorDialog.PHOTO_SIZE_PX, CropImageView.RequestSizeOptions.RESIZE_EXACT)
.setActivityTitle(context.getString(R.string.profile_choose_photo))
.setAllowFlipping(false)
.start(context, this)
}
else if (requestCode == CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE) {
val result = CropImage.getActivityResult(data)
if (resultCode == Activity.RESULT_OK) {
val resultUri = result.uri
try {
//Save the image to disk
val DATE_FORMAT_YEAR_MON_DAY_HR_MIN_SEC = "yyyyMMdd_HHmmss"
val dateSuffix = String.format(Locale.getDefault(), "%s", DateFormat.format(DATE_FORMAT_YEAR_MON_DAY_HR_MIN_SEC, Date()))
val bitmap = MediaStore.Images.Media.getBitmap(context.contentResolver, resultUri)
val imageFile = bitmap.save(context, imagePrefix!!, dateSuffix, "jpg")
updateImage(imageFile)
} catch (ioe: IOException) {
Timber.e("IOException decoding bitmap")
}
} else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
val error = result.error
Timber.e(error)
}
}
}
fun handleRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
when (requestCode) {
RESULT_STORAGE_PERMISSION -> {
// If request is cancelled, the result arrays are empty.
if (grantResults.isNotEmpty()
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// permission was granted, yay!
Timber.d("Storage access granted")
promptToTakeOrSelectPhoto(requireContext())
} else {
Timber.d("User disabled permission! Can't select a photo!")
}
}
}
}
companion object {
const val RESULT_STORAGE_PERMISSION = 45432
const val PICK_PHOTO_REQUEST_CODE = 8285
const val TAKE_PHOTO_REQUEST = 112
}
}
fun Bitmap.save(context: Context, fileNamePrefix: String, fileNameSuffix: String, fileExtension: String): File? {
try {
//Save the image to disk
val file = context.filesDir
var imageFile: File? = null
if (file != null) {
try {
val filePathForNewImage = "$fileNamePrefix-$fileNameSuffix.$fileExtension"
imageFile = File(file, filePathForNewImage)
val stream = FileOutputStream(imageFile, false)
Timber.d("start saving to: %s", imageFile.absolutePath)
val complete = compress(Bitmap.CompressFormat.JPEG, 60, stream)
Timber.d("done saving...")
if (!complete) {
Timber.d("image didn't save")
}
Timber.d("image saved")
} catch (e: IOException) {
Timber.d(e, "Can't save image")
}
}
return imageFile
} catch (e: Exception) {
Timber.e("Save image failed: %s", e.toString())
return null
}
}
dialog_simple_yes_no.xml
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:minWidth="@dimen/dialog_min_width"
android:orientation="vertical"
android:background="@android:color/white"
android:padding="@dimen/margin_large">
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/dialog_title"
android:gravity="center"
android:maxLines="4"
android:textSize="20sp"
android:layout_marginTop="@dimen/margin_medium"
android:layout_marginBottom="@dimen/margin_large"
tools:text="This Is A Title"
/>
<androidx.appcompat.widget.AppCompatTextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/dialog_message"
android:gravity="left"
android:textSize="15sp"
android:layout_marginTop="@dimen/margin_medium"
android:layout_marginBottom="@dimen/margin_large_xl"
tools:text="This is a nice dialog message. It's easy to interact with this dialog.\n\nIf you need to space out the text, you can even use a slash n." />
<androidx.appcompat.widget.AppCompatButton
android:layout_width="match_parent"
android:id="@+id/dialog_yes_btn"
tools:text="@android:string/yes"
android:layout_marginBottom="@dimen/margin_large"
/>
<androidx.appcompat.widget.AppCompatButton
android:layout_width="match_parent"
android:id="@+id/dialog_no_btn"
tools:text="@android:string/no"
android:text="@string/login_not_now"
/>
</LinearLayout>
Hey!
I start a new project to handover this library https://github.com/CanHub/Android-Image-Cropper
The ideia is that we keep improving because this project don’t have updates since 2018
Hope I can count with your help.
Open to contribute, next pieces of work will be Android 11 permissions, refactor into Kotlin and ActivityContract
manifest
Done and save my day.
manifest
thanks i solved with this
When pulling this library into a application the cropper does not work as expected on Android R / 11.
If a user gives permission to access Camera on Android 10 and below a user has the ability to choose from the Camera and the Gallery on the device.
Android R/11 the user is prompted with the view to choose but it says 'No apps can perform this action.'