AndroidResultContracts.TakePicture() returns Boolean instead of Bitmap
Asked Answered
P

2

6

The contract has been changed to return Boolean instead of Bitmap starting in androidx.activity version 1.2.0-alpha05. How can I use the Boolean returned by the built in AndroidResultContracts.TakePicture() contract to access and display the photo just taken by the user?

Protonema answered 13/8, 2020 at 21:26 Comment(0)
P
10

I am using

    implementation 'androidx.activity:activity-ktx:1.2.0-alpha07'
    implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha07'

Here's my full sample code showing how to use the built-in Android Result Contract to take a photo from your application and display it in an ImageView.

Note: My solution uses View Binding

MainActivity's layout XML included (1) a button defining onTakePhotoClick as the onClick event and (2) and ImageView to display the photo taken.

        <Button
            android:id="@+id/take_photo_button"
            style="@style/Button"
            android:drawableStart="@drawable/ic_camera_on"
            android:onClick="onTakePhotoClick"
            android:text="@string/button_take_photo"
            app:layout_constraintTop_toBottomOf="@id/request_all_button" />

        ...

        <ImageView
            android:id="@+id/photo_preview"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            app:layout_constraintTop_toBottomOf="@id/take_video_button" />

In my MainActivity I have done the following:

  1. Defined imageUri: Uri? which will be set to the uri of the image taken by the TakePicture() contract.
  2. Implemented onTakePhotoClick() to check for the necessary camera permissions before launching the TakePicture() contract.
  3. Defined takePictureRegistration: ActivityResultLauncher which will actually launch the request to take a photo on the device. When isSuccess is returned as true then I know the imageUri I previously defined now references the photo I just took.
  4. Defined a takePicture: Runnable simply for code reuse. Note that the 2nd String parameter passed to the FileProvider.getUriForFile(context, authority, file) method will need to match the authorities attribute provided to the <provider> in your app's AndroidManifest.xml.
  5. For full transparency, I have also added the code showing how I use the ActivityResultContracts.RequestPermission() to request the user for runtime permissions to access the camera.
    private var imageUri: Uri? = null

    /**
     * Function for onClick from XML
     *
     * Check if camera permission is granted, and if not, request it
     * Once permission granted, launches camera to take photo
     */
    fun onTakePhotoClick(view: View) {
        if (!checkPermission(Manifest.permission.CAMERA)) {
            // request camera permission first
            onRequestCameraClick(callback = takePicture)
        } else {
            takePicture.run()
        }
    }

    private val takePicture: Runnable = Runnable {
        ImageUtils.createImageFile(applicationContext)?.also {
            imageUri = FileProvider.getUriForFile(
                applicationContext,
                BuildConfig.APPLICATION_ID + ".fileprovider",
                it
            )
            takePictureRegistration.launch(imageUri)
        }
    }

    private val takePictureRegistration =
        registerForActivityResult(ActivityResultContracts.TakePicture()) { isSuccess ->
            if (isSuccess) {
                mBinding.photoPreview.setImageURI(imageUri)
            }
        }

    /**
     * Function for onClick from XML
     *
     * Launches permission request for camera
     */
    fun onRequestCameraClick(view: View? = null, callback: Runnable? = null) {
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
            // update image
            mBinding.iconCameraPermission.isEnabled = isGranted

            val message = if (isGranted) {
                "Camera permission has been granted!"
            } else {
                "Camera permission denied! :("
            }

            Toast.makeText(this, message, Toast.LENGTH_SHORT).show()

            if (isGranted) {
                callback?.run()
            }
        }.launch(Manifest.permission.CAMERA)
    }

For full transparency the ImageUtils utility class has the createImageFile() method defined as follows and returns a File? when given context. Note that I am using the external files directory as the storage directory for my FileProvider.

object ImageUtils {
    lateinit var currentPhotoPath: String

    @Throws(IOException::class)
    fun createImageFile(context: Context): File? {
        // Create an image file name
        val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
        val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
        return File.createTempFile(
            "JPEG_${timeStamp}_", /* prefix */
            ".jpg", /* suffix */
            storageDir /* directory */
        ).apply {
            // Save a file: path for use with ACTION_VIEW intents
            currentPhotoPath = absolutePath
        }
    }
}

Don't forget to add the uses-permission, uses-feature and provider tags to the AndroidManifest.

Also make sure the authorities attribute provided to the <provider> matches the 2nd String parameter passed to FileProvider.getUriForFile(context, authority, file) method. In my example, I have made my authority the package name + ".fileprovider". Read more about FileProvider from Google's documentation.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.captech.android_activity_results">

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <uses-feature android:name="android.hardware.camera" />

    <application
        ...

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.captech.android_activity_results.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>
    </application>
</manifest>

My res/xml/file_paths is shown below. Because I am using getExternalFilesDir(), I am using the <external-files-path> tags in the XML.

Note: If you are NOT using the external files directory, you may want to look up which FileProvider storage directory you want to specify in your XML tags here.

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <external-files-path
        name="my_images"
        path="/" />
</paths>

The result would display the imageUri in the ImageView:

The image that was just taken by the device camera is being displayed in an ImageView in the application

Protonema answered 13/8, 2020 at 21:26 Comment(3)
You don't need the CAMERA permission to take a photo via an external app. That app is what needs that permission. It will throw a SecurityException, though, if it's listed in the manifest but not granted, so make sure to remove everything, if you change that. Also, you're not really using WRITE_EXTERNAL_STORAGE, since you're not requesting it, and you don't need it for getExternalFilesDir() anyway, so you could get rid of that, too. Just FYI.Nisus
The fact that all of these parts are needed in order to simply get an image from the default camera app using this new and supposedly improved API is utterly absurd. Why does the API not have an option to autogenerate a temp Uri? Why is there no API to get a Bitmap where that is what's needed? Google, please fix this.Dna
Your code is cleaner than the code written in the official documentation about AndroidResultContract {which doesn't exist}, and the one written here developer.android.com/training/camera/photobasics?authuser=1Beverlee
A
3

I've been struggling with this same issue and only just came up with a (somewhat) tenable solution involving ContentResolver.

The documentation leaves a lot to the imagination. A major concern with this approach is that the captured image URI has to be managed external to the ActivityResultContract, which seems counterintuitive as the original question already points out.

I do not know of another way to insert media into the gallery that would solve that part of the problem, but I would absolutely love to see that solution.

// Placeholder Uri
var uri: Uri? = null

// ActivityResultContract for capturing an image
val takePicture =
    registerForActivityResult(contract =
        ActivityResultContracts.TakePicture()) { imageCaptured ->
            if (imageCaptured) {
                // Do stuff with your Uri here
            }
        }

...

fun myClickHandler() {
    // Create a name for your image file
    val filename = "${getTimeStamp("yyyyMMdd_HHmmss")}-$label.jpg"

    // Get the correct media Uri for your Android build version
    val imageUri =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Images.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL_PRIMARY)
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }
        val imageDetails = ContentValues().apply {
        put(MediaStore.Audio.Media.DISPLAY_NAME, filename)
    }

    // Try to create a Uri for your image file
    activity.contentResolver.insert(imageUri, imageDetails).let {
        // Save the generated Uri using our placeholder from before
        uri = it

        // Capture your image
        takePicture.launch(uri)
    } ?: run {
        // Something went wrong
    }
}
Ashanti answered 7/4, 2021 at 19:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.