ActivityResultContracts.TakePicture - Image is not saved after activity orientation change
Asked Answered
S

1

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
}
Scourings answered 26/9, 2022 at 10:32 Comment(14)
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.Happygolucky
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.Happygolucky
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"Chymotrypsin
@Happygolucky 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: medium.com/codex/… I have updated source code in question.Scourings
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.Happygolucky
@Chymotrypsin This is pseudocode. I added onClick call in source code in question for better clarity. About tmpImageUri reset - good idea, I'll check it.Scourings
Which Android version of used device? Happygolucky
have updated source code in question. ?? There is still File.createTmpFile().Happygolucky
@Happygolucky I removed useless lines createNewFile() and deleteOnExit() in File.createTempFile("tmp", ".jpg", context.cacheDir)Scourings
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 thisChymotrypsin
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...Chymotrypsin
@Chymotrypsin 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 :)Scourings
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.Happygolucky
@Chymotrypsin 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.Scourings
S
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)
    }
    ...
}
Scourings answered 29/9, 2022 at 8:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.