Store full-size images using a FileProvider

119 Views Asked by At

I want to save an image in its full size. I use a file provider for this. I can take the picture, but I don't get any data in the onActivityResult function. The intent is null. What am I doing wrong?

provider_path.xml:

<?xml version="1.0" encoding="utf-8"?>
 <paths xmlns:android="http://schemas.android.com/apk/res/android">
 <root-path name="root" path="." />
</paths>

provider in AndroidManifest.xml:

<provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.bkoubik.longlesstime.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_path"/>
</provider>

My Fragment Class:

fun capturePhoto(){
    val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
    val fileUri:File = createImageFile();

    val photoURI = FileProvider.getUriForFile(
        requireActivity(),
        "com.bkoubik.longlesstime.fileprovider",
        fileUri
    )
    intent.putExtra( MediaStore.EXTRA_OUTPUT, photoURI )
    startActivityForResult(intent,IMAGE_REQUEST)
}

 private fun createImageFile(): File {
    val wrapper = ContextWrapper(requireContext())
    var photoFile = wrapper.getDir(IMAGE_DIRECTORY,Context.MODE_PRIVATE)
    photoFile = File(photoFile,"${UUID.randomUUID()}.jpg")
    return photoFile
 }

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
  super.onActivityResult(requestCode, resultCode, data)

  if(requestCode == IMAGE_REQUEST && resultCode == Activity.RESULT_OK && data != null)
        {
            //data = null
            val imgData = data.data!!

        ...
        }
  }

companion object {
    private val IMAGE_DIRECTORY = "long_less"
}

enter image description here

Error:

java.lang.IllegalArgumentException: Failed to find configured root that contains /data/data/com.bkoubik.longlesstime/app_long_less/1219a0bd-c17e-4709-b880-1e4f549362f6.jpg

1

There are 1 best solutions below

0
Ihor Baklykov On

First of all, I'll strongly recommend you to use new contracts API which is also recommended by Google:

While the underlying startActivityForResult() and onActivityResult() APIs are available on the Activity class on all API levels, it is strongly recommended to use the Activity Result APIs introduced in AndroidX Activity and Fragment. (see Android docs)

Than, when photo was taken from GUI, I'm getting photo data from provider like this (it's my separate camera request Activity). Also, I'm using external cache directory because on my Android 10.0 device data folder is not accessible (see Android docs)

P.S. Take a note about grantUriPermission method call on photo URI

    // URI for photo file
    private lateinit var mPhotoUri  : Uri

    /// Photo file itself
    private lateinit var mPhotoFile : File


    // Camera permission request
    private val mRequestCamera      = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
        // Camera permission request parse
        when (result.resultCode) {
            // Success
            RESULT_OK       -> {
                // Photo request
                mRequestPhoto.launch(mPhotoUri)
            }
            // No permisison
            else            -> {
                // Close activity
                finish()
            }
        }
    }


    // Photo request
    private val mRequestPhoto       = registerForActivityResult(ActivityResultContracts.TakePicture()) { result ->
        // Check request result
        when (result) {
            // Success
            true -> {
                // Photo from media stream
                val photoData = BitmapFactory.decodeStream(
                    contentResolver.openInputStream(mPhotoUri)
                )
                // Stream for photo save
                val dataStream = ByteArrayOutputStream()
                // Compress photo as JPEG of 90% quality
                photoData.compress(
                    Bitmap.CompressFormat.JPEG,
                    90,
                    dataStream
                )
                // Coroutine to write photo
                CoroutineScope(Dispatchers.Main).launch {
                    // Inside IO context
                    withContext(Dispatchers.IO) {
                        // File output stream
                        FileOutputStream(mPhotoFile).also { fileStream ->
                            // Write JPEG image data to file
                            fileStream.write(dataStream.toByteArray())
                            // Close JPEG image data stream
                            dataStream.close()
                            // Close file stream
                            fileStream.close()
                        }
                    }
                }
                // Done here - notify someone photo is ready
                sendBroadcast(
                    Intent(
                        <Where/Whom to send>
                    ).apply {
                        // Content provider URI
                        putExtra(
                            <Photo URI extra name>,
                            mPhotoUri
                        )
                    }
                )
                // Close activity
                finish()
            }
            // Error
            else -> {
                // Close activity
                finish()
            }
        }
    }


    // Activity onCreate method
    @Override
    override fun onCreate(savedInstanceState: Bundle?) {
        // Parent method call
        super.onCreate(savedInstanceState)
        // Try
        try {
            // Create temporary file
            mPhotoFile      = File.createTempFile(
                "<file prefix>",
                "<file suffix>",
                externalCacheDir
            ).apply {
                // Make it writable
                setWritable(true)
            }
            // Get file URI (Android M+ ?)
            mPhotoUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                // Trying to get provider (I send it to this Activity via Intent with extra "EXTRA_AUTHORITY")
                intent?.extras?.getString("EXTRA_AUTHORITY")
                    ?.let { authority ->
                        // For Android 7.0+ getting FileProvider
                        FileProvider.getUriForFile(
                            applicationContext,
                            authority,
                            mPhotoFile
                        )
                        // Else for Android 7.0 or lower
                    } ?: Uri.fromFile(mPhotoFile)
            } else {
                // Else for Android 7.0 or lower
                Uri.fromFile(mPhotoFile)
            }
            // Permissions for URI itself
            grantUriPermission(
                packageName,
                mPhotoUri,
                Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
            )
        // Catch
        } catch (e: Exception) {
            // Stacktrace to logcat
            e.printStackTrace()
            // Close camera activity
            finish()
        }
        // No camera permissions ?
        if (!ActivityPermissionsCamera.hasPermissions(applicationContext)) {
            // Intent for camera permissions
            val permissionsIntent = Intent(
                applicationContext,
                <Permissions for Camera Activity>::class.java
            ).apply {
                // New task
                flags = Intent.FLAG_ACTIVITY_NEW_TASK
            }
            // Request permissions
            mRequestCamera.launch(permissionsIntent)
        } else {
            // Camera launch
            mRequestPhoto.launch(mPhotoUri)
        }
    }

And this is how I reques photo from camera:

    // Photo pending intent
    private fun createPendingIntentPhoto() : PendingIntent? {
        // Runtime check - any camera ?!
        if (!applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
            // No camera - no photo !!!
            return null
        }
        // Intent flags
        val intentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            PendingIntent.FLAG_IMMUTABLE or
            PendingIntent.FLAG_UPDATE_CURRENT
        } else {
            PendingIntent.FLAG_UPDATE_CURRENT
        }
        // Pending intent
        return PendingIntent.getActivity(
            applicationContext,
            1,
            Intent(
                applicationContext,
                <Activity for Camera>::class.java
            ).apply {
                // Pass provider as Intent extra
                putExtra("EXTRA_AUTHORITY", "${BuildConfig.APPLICATION_ID}.provider")
            },
            intentFlags
        )
    }

P.P.S. Quick note on why do I pass provider URI over Intent's extra - my camera routines are in separate library which is used in GUI-project So they can have different BuildConfig.APPLICATION_ID. This allows me to use same library in many projects.