0

I'm using Activity Result API (with FileProvider) to save image from Camera to app cache folder and then pass it URI to another activity.

It works if activity orientation does not change during operation.

But when user rotate Camera activity (launched by ActivityResultContracts.TakePicture contract) - image will not be saved (file created with 0 bytes). And I'm getting error while decoding image in activity where I'm pass URI:

android.graphics.ImageDecoder$DecodeException: Failed to create image decoder with message 'unimplemented'Input contained an error.

I tried to solve problem in obvious way - programmatically lock orientation before camera callback launch and unlock after, but this only works the first time (may sound strange, but that's how it is), when orientation is changed again - problem persists. It looks like after changing orientation connection between current activity result callback and the Camera activity is broken. After research, it turned out that if I do NOT launch activity to open image - image is correctly saved. I tried to launch activity with delay and in another thread - but without success.

I still can't find reason why image doesn't save when orientation changed.

screenshot

Main activity:

class MainActivity : AppCompatActivity() {
    private val tmpImageUri by lazy { FileUtils.getTmpFileUri(applicationContext) }
    
    private val takeImageResult = registerForActivityResult(ActivityResultContracts.TakePicture()) { isSuccess ->
        if (isSuccess) {
            // We have successfully (actually no) saved the image and can work with it via tmpImageUri
            launchEditActivity(tmpImageUri)
        }
    }

    private fun takePictureFromCamera() {
        takeImageResult.launch(tmpImageUri)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding.button.setOnClickListener {
            takePictureFromCamera()
        }
    }

    private fun launchEditActivity(uri: Uri){
        val intent = Intent(this, EditActivity::class.java).apply {
            data = uri
        }
        startActivity(intent)
    }
}

Activity where I'm pass URI:

class EditActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        loadImage()
    }

    private fun loadImage(){
        intent.data?.let { uri ->
            model.viewModelScope.launch(Dispatchers.IO) {
                val bitmap = FileUtils.loadBitmapFromUri(applicationContext, uri)
                runOnUiThread {
                    model.setImage(bitmap)
                }
            }
        }
    }
}

File utils:

fun getTmpFileUri(context: Context): Uri {
    clearCache(context)
    val tmpFile = File.createTempFile("tmp", ".jpg", context.cacheDir)
    return FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", tmpFile)
}

private fun clearCache(context: Context){
    context.cacheDir.deleteRecursively()
}

fun loadBitmapFromUri(context: Context, uri: Uri): Bitmap{
    val bitmap: Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
        val src = ImageDecoder.createSource(context.contentResolver, uri)
        ImageDecoder.decodeBitmap(src) { decoder, info, source ->
            decoder.isMutableRequired = true
        }
    } else {
        MediaStore.Images.Media.getBitmap(context.contentResolver, uri)
    }
    return bitmap
}
Mikhail
  • 2,612
  • 3
  • 22
  • 37
  • Which Android version of used device? When exactly is the device rotated? Before or after taking the image? `(file created with 0 bytes)` But that empty file is not created by the camera app but by your -wrong- code using `createNewFile()`. Do not create the file already. You only need that File instance with the right path. – blackapps Sep 26 '22 at 11:12
  • `And I'm getting error while decoding image in activity where I'm pass URI:` You could simply check existence of file and file size before calling that statement. – blackapps Sep 26 '22 at 11:18
  • first starting another activity inside onCreate without finishing current is terible idea, second after rotation your `tmpImageUri` is "reset" (as new instance of MainActivity is created) - you should use obvious way and store previous uri in "savedInstance" – Selvin Sep 26 '22 at 11:19
  • @blackapps Bug reproduces everywhere (does not depend on android version or device). Device rotated when Camera activity is open (TL;DR - Camera activity orientation must be opposite Main activity orientation at the moment where we confirm taken picture). Thank you for noticing the useless file creation code. I followed this article: https://medium.com/codex/how-to-use-the-android-activity-result-api-for-selecting-and-taking-images-5dbcc3e6324b I have updated source code in question. – Mikhail Sep 26 '22 at 12:22
  • `first starting another activity inside onCreate without finishing current is terible idea` May be. But i do not see that in the code. Its done in on activity result. @Selvin. – blackapps Sep 26 '22 at 12:22
  • @Selvin This is pseudocode. I added onClick call in source code in question for better clarity. About tmpImageUri reset - good idea, I'll check it. – Mikhail Sep 26 '22 at 12:22
  • `Which Android version of used device? ` – blackapps Sep 26 '22 at 12:23
  • `have updated source code in question.` ?? There is still File.createTmpFile(). – blackapps Sep 26 '22 at 12:26
  • @blackapps I removed useless lines `createNewFile()` and `deleteOnExit()` in `File.createTempFile("tmp", ".jpg", context.cacheDir)` – Mikhail Sep 26 '22 at 12:30
  • again ... when new instance of `MainActivity` is created your `FileUtils.getTmpFileUri` is called and old temp file is deleted and new one is created ... you need to save `tmpImageUri` somewhere and then get it back when activity is recreated - I would use `savedInstanceState` for this – Selvin Sep 26 '22 at 12:30
  • and yeah, I think that google did a mistake and `ActivityResultContracts.TakePicture` should use different `ActivityResultCallback` which not only returns boolean but also passed uri back... – Selvin Sep 26 '22 at 12:36
  • @Selvin I'm still investigating that issue where tmp file uri reset. I'll write separately and highlight this if it's a solution. Don't rush :) – Mikhail Sep 26 '22 at 12:36
  • How would you remove code from File.createTempFile() ? Its not your function. Its an Android function. In anyway reading that code is not nice as it contains create. `val tmpFile = File(context.cacheDir, "tmp.jpg")` would be better. – blackapps Sep 26 '22 at 12:39
  • @Selvin Thanks, the problem really was that the `tmpImageUri` was being reset when the activity rotated. After saving and restoring it in "savedInstance" everything started working as it should. Please post your answer so I can accept it. – Mikhail Sep 26 '22 at 13:45

1 Answers1

1

The problem was in tmpImageUri variable which was reset on orientation change. After saving and restoring it in savedInstance everything started working as it should. Thanks @Selvin for help.

Fixed Main activity:

class MainActivity : AppCompatActivity() {
    companion object {
        private const val KEY_TMP_FILE_URI = "tmpFileUri"
    }

    private var tmpImageUri: Uri? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        tmpImageUri =
            if(savedInstanceState?.containsKey(KEY_TMP_FILE_URI) == true)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
                    savedInstanceState.getParcelable(KEY_TMP_FILE_URI, Uri::class.java)
                else savedInstanceState.getParcelable(KEY_TMP_FILE_URI)
            else FileUtils.getTmpFileUri(applicationContext)
        ...
    }

    private val takeImageResult = registerForActivityResult(ActivityResultContracts.TakePicture()) { isSuccess ->
        if (isSuccess) {
            tmpImageUri?.let { launchEditActivity(it) }
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putParcelable(KEY_TMP_FILE_URI, tmpImageUri)
    }
    ...
}
Mikhail
  • 2,612
  • 3
  • 22
  • 37