Android-Image-Cropper icon indicating copy to clipboard operation
Android-Image-Cropper copied to clipboard

Doesnt work on Android R / Android 11

Open adfwhitestar opened this issue 3 years ago • 20 comments

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.' Screen Shot 2020-09-28 at 12 58 33 PM

adfwhitestar avatar Sep 28 '20 16:09 adfwhitestar

Yes, same issue on my Pixel 3XL device after upgrade to Android 11

bluesclues9 avatar Sep 29 '20 12:09 bluesclues9

Same issue being reported by hundreds of users of our apps. Anyone have a workaround for Android 11?

matthewkrueger avatar Oct 01 '20 04:10 matthewkrueger

Same. I downgraded the my targetSdkVersion to 29 instead of 30 and the image doesn't update.

garbageOscar avatar Oct 01 '20 12:10 garbageOscar

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

amsmokefree avatar Oct 01 '20 15:10 amsmokefree

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.

matthewkrueger avatar Oct 01 '20 15:10 matthewkrueger

device-2020-10-02-124101 I have the same, sometimes it works sometimes it doesn't Pixel4 Android11

MobilefactoryAT avatar Oct 02 '20 10:10 MobilefactoryAT

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
        )
    }

svcorporate avatar Oct 05 '20 13:10 svcorporate

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.

adfwhitestar avatar Oct 06 '20 21:10 adfwhitestar

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 😅

korva avatar Oct 07 '20 07:10 korva

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

fAntel avatar Oct 08 '20 09:10 fAntel

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 avatar Oct 11 '20 14:10 ajackson2907

@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 avatar Oct 12 '20 08:10 fAntel

@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 avatar Oct 12 '20 19:10 ajackson2907

@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 avatar Oct 12 '20 19:10 matthewkrueger

@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

ajackson2907 avatar Oct 12 '20 19:10 ajackson2907

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. image

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>

matthewkrueger avatar Nov 12 '20 18:11 matthewkrueger

please check this

Canato avatar Nov 16 '20 21:11 Canato

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

Canato avatar Nov 27 '20 16:11 Canato

manifest

Done and save my day.

Heena21 avatar Sep 03 '21 01:09 Heena21

manifest

<intent>
    <action android:name="android.media.action.IMAGE_CAPTURE"/>
</intent>

thanks i solved with this

nvcc1701 avatar May 14 '22 05:05 nvcc1701