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

Doesnt work on Android R / Android 11 #788

Open
adfwhitestar opened this issue Sep 28, 2020 · 20 comments
Open

Doesnt work on Android R / Android 11 #788

adfwhitestar opened this issue Sep 28, 2020 · 20 comments

Comments

@adfwhitestar
Copy link

adfwhitestar commented Sep 28, 2020

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

@bluesclues9
Copy link

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

@matthewkrueger
Copy link

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

@garbageOscar
Copy link

garbageOscar commented Oct 1, 2020

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

@amsmokefree
Copy link

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

@matthewkrueger
Copy link

matthewkrueger commented Oct 1, 2020

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: #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.

@MobilefactoryAT
Copy link

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

@svcorporate
Copy link

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

@adfwhitestar
Copy link
Author

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.

@korva
Copy link

korva commented Oct 7, 2020

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 😅

@fAntel
Copy link

fAntel commented Oct 8, 2020

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

@ajackson2907
Copy link

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?

@fAntel
Copy link

fAntel commented Oct 12, 2020

@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
Copy link

ajackson2907 commented Oct 12, 2020

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

@matthewkrueger
Copy link

@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
Copy link

@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

@matthewkrueger
Copy link

matthewkrueger commented Nov 12, 2020

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>

@Canato
Copy link

Canato commented Nov 16, 2020

please check this

@Canato
Copy link

Canato commented Nov 27, 2020

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

@Heena21
Copy link

Heena21 commented Sep 3, 2021

manifest

Done and save my day.

@nvcc1701
Copy link

manifest

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

thanks i solved with this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests