Search code examples
androidkotlinstack-overflow

Fragment navigation bug Android 13


So basically I have an app where I move across different fragments by using NavController navigation between fragments. This is working on all Android versions < 13.

findNavController().navigate(R.id.step02Fragment)

The issue on Android 13 is that when I move from the Step05Fragment to the Step06Fragment the onPause and onResume methods of the Step05Fragment start to execute forever and ever, causing a StackOverflow error.

I have no idea why this is happening between these two steps and in this specific Android version. All other fragments do the navigation the exact same way also and this issue doesn't happen on them.

Any ideas?

Thanks

P.D These are the libraries versions, if this is a known issue on any of them

implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "androidx.lifecycle:lifecycle-service:2.5.1"

implementation "androidx.navigation:navigation-runtime-ktx:2.5.2"
implementation "androidx.navigation:navigation-fragment-ktx:2.5.2"
implementation "androidx.navigation:navigation-ui-ktx:2.5.2"
implementation "androidx.navigation:navigation-dynamic-features-fragment:2.5.2"

Here is the code of the fragments

@AndroidEntryPoint
class Step05Fragment : Fragment() {

    lateinit var binding: FragmentStep05Binding

    private var initialSetup: Boolean = true
    private val disposable = CompositeDisposable()

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        binding = FragmentStep05Binding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        arguments?.getBoolean(Constants.ENABLE_PERMISSION)?.let {
            initialSetup = it
        }

        setObservables()
        setView()
    }

    override fun onDestroy() {
        super.onDestroy()
        disposable.dispose()
    }

    private fun setObservables() {
        binding.btnEnable.clicks()
            .map {
                checkPermission()
            }
            .subscribe()
            .disposeBy(disposable)
    }

    private fun setView() {
        if (!initialSetup) {
            binding.currentStep.visibility = View.GONE
        }
    }

    private fun checkPermission() {
        if (!PermissionsUtils.isReadExternalStoragePermissionGranted(requireContext())) {
            requestPermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
        } else {
            handleNavigation()
        }
    }

    private val requestPermission =
        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
            if (isGranted) {
                handleNavigation()
            } else {
                checkPermission()
            }
        }

    private fun handleNavigation() {
        if (initialSetup) {
            findNavController().navigate(R.id.step06Fragment)
        } else {
            activity?.finishAffinity()
        }
    }

}

@AndroidEntryPoint
class Step06Fragment : Fragment() {

lateinit var binding: FragmentStep06Binding

private var initialSetup: Boolean = true
private val disposable = CompositeDisposable()

override fun onCreateView(
    inflater: LayoutInflater,
    container: ViewGroup?,
    savedInstanceState: Bundle?
): View {
    binding = FragmentStep06Binding.inflate(inflater, container, false)
    return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    arguments?.getBoolean(Constants.ENABLE_PERMISSION)?.let {
        initialSetup = it
    }

    setObservables()
    setView()
}

override fun onDestroy() {
    super.onDestroy()
    disposable.dispose()
}

private fun setObservables() {
    binding.btnEnable.clicks()
        .map {
            checkPermission()
        }
        .subscribe()
        .disposeBy(disposable)
}

private fun setView() {
    if (!initialSetup) {
        binding.currentStep.visibility = View.GONE
    }
}

private fun checkPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        if (!PermissionsUtils.isMediaLocationPermissionGranted(requireContext())) {
            requestPermission.launch(Manifest.permission.ACCESS_MEDIA_LOCATION)
        } else {
            handleNavigation()
        }
    } else {
        handleNavigation()
    }
}

private val requestPermission =
    registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        if (isGranted) {
            handleNavigation()
        } else {
            checkPermission()
        }
    }

private fun handleNavigation() {
    if (initialSetup) {
        findNavController().navigate(R.id.step07Fragment)
    } else {
        activity?.finishAffinity()
    }
}

Solution

  • After a while, found the answer

    https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions

    Seems like this weird behavior is because of this, added the following validation after updating my project dependencies to aim Android 13 and it worked just fine

    private fun checkPermission() {
            if (!PermissionsUtils.isReadExternalStoragePermissionGranted(requireContext())) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                    requestPermission.launch(Manifest.permission.READ_MEDIA_IMAGES)
                } else {
                    requestPermission.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
                }
            } else {
                handleNavigation()
            }
        }