fondesa/kpermissions

permissionsBuilder .sendSuspend() not completing first time

GuilhE opened this issue ยท 31 comments

Description

Using version 3.2.0, after given Location Permissions, sendSuspend() doesn't return, in other words, my app freezes the UI (it doesn't get rendered because it needs location permission and since sendSuspend() doesn't complete I can't query result.allGranted() and proceed with logic.
If I navigate back and then forward it works (the permission wore granted and no pop-up is shown, I can query result.allGranted() happily and carry on).

If I downgrade to 3.1.3 it works as expected.


KPermissions version:
3.2.0

API level:
SDK 30, Google Pixel 3a Android 11

How to reproduce it:
Clear app cache and run it so that permissions are prompt again.

Sample code:

lifecycleScope.launchWhenResumed {
    val result =
    permissionsBuilder(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION).build().sendSuspend()
    if (result.allGranted()) { ... }
}

@GuilhE I'll try to reproduce it, thanks.

In the meanwhile, can you try to call in your Application the method useLegacyRuntimePermissionHandler() using the version 3.2.0 and tell me if it fixes your problem?

As another try, can you try adding the following class to your project using the version 3.2.0?

import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.fondesa.kpermissions.PermissionStatus
import com.fondesa.kpermissions.allGranted
import com.fondesa.kpermissions.extension.isPermissionGranted
import com.fondesa.kpermissions.request.runtime.RuntimePermissionHandler
import com.fondesa.kpermissions.request.runtime.RuntimePermissionHandlerProvider

internal class CustomRuntimePermissionHandlerProvider(private val manager: FragmentManager) : RuntimePermissionHandlerProvider {
    @RequiresApi(23)
    override fun provideHandler(): RuntimePermissionHandler {
        var fragment = manager.findFragmentByTag(FRAGMENT_TAG) as? RuntimePermissionHandler
        if (fragment == null) {
            fragment = CustomRuntimePermissionsHandler()
            manager.beginTransaction()
                .add(fragment, FRAGMENT_TAG)
                .commitAllowingStateLoss()
        }
        return fragment
    }

    @RequiresApi(23)
    class CustomRuntimePermissionsHandler : Fragment(), RuntimePermissionHandler {
        private val resultLauncher = registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions(),
            ::onPermissionsResult
        )
        private val listeners = mutableMapOf<Set<String>, RuntimePermissionHandler.Listener>()
        private var pendingHandleRuntimePermissions: (() -> Unit)? = null
        private var pendingPermissions: Array<out String>? = null

        override fun onAttach(context: Context) {
            super.onAttach(context)
            pendingHandleRuntimePermissions?.invoke()
            pendingHandleRuntimePermissions = null
        }

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            if (pendingPermissions == null) {
                pendingPermissions = savedInstanceState?.getStringArray(KEY_PENDING_PERMISSIONS)
            }
        }

        override fun onSaveInstanceState(outState: Bundle) {
            super.onSaveInstanceState(outState)
            outState.putStringArray(KEY_PENDING_PERMISSIONS, pendingPermissions)
        }

        override fun attachListener(permissions: Array<out String>, listener: RuntimePermissionHandler.Listener) {
            listeners[permissions.toSet()] = listener
        }

        override fun handleRuntimePermissions(permissions: Array<out String>) {
            if (isAdded) {
                handleRuntimePermissionsWhenAdded(permissions)
            } else {
                pendingHandleRuntimePermissions = { handleRuntimePermissionsWhenAdded(permissions) }
            }
        }

        @Suppress("OverridingDeprecatedMember")
        override fun requestRuntimePermissions(permissions: Array<out String>) {
            pendingPermissions = permissions
            Log.d(TAG, "requesting permissions: ${permissions.joinToString()}")
            resultLauncher.launch(permissions)
        }

        private fun handleRuntimePermissionsWhenAdded(permissions: Array<out String>) {
            val listener = listeners[permissions.toSet()] ?: return
            val activity = requireActivity()
            val currentStatus = activity.checkRuntimePermissionsStatus(permissions.toList())
            val areAllGranted = currentStatus.allGranted()
            if (!areAllGranted) {
                if (pendingPermissions != null) return
                @Suppress("DEPRECATION")
                requestRuntimePermissions(permissions)
            } else {
                listener.onPermissionsResult(currentStatus)
            }
        }

        private fun onPermissionsResult(permissionsResult: Map<String, Boolean>) {
            val pendingPermissions = pendingPermissions ?: return
            this.pendingPermissions = null
            val listener = listeners[pendingPermissions.toSet()] ?: return
            val result = permissionsResult.map { (permission, isGranted) ->
                when {
                    isGranted -> PermissionStatus.Granted(permission)
                    shouldShowRequestPermissionRationale(permission) -> PermissionStatus.Denied.ShouldShowRationale(permission)
                    else -> PermissionStatus.Denied.Permanently(permission)
                }
            }
            listener.onPermissionsResult(result)
        }

        private fun Activity.checkRuntimePermissionsStatus(permissions: List<String>): List<PermissionStatus> =
            permissions.map { permission ->
                if (isPermissionGranted(permission)) {
                    return@map PermissionStatus.Granted(permission)
                }
                if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
                    PermissionStatus.Denied.ShouldShowRationale(permission)
                } else {
                    PermissionStatus.RequestRequired(permission)
                }
            }

        companion object {
            private val TAG = CustomRuntimePermissionHandlerProvider::class.java.simpleName
            private const val KEY_PENDING_PERMISSIONS = "pending_permissions"
        }
    }

    companion object {
        private const val FRAGMENT_TAG = "CustomPermissionsFragment"
    }
}

After doing that, you should integrate it with:

lifecycleScope.launchWhenResumed {
    val result = permissionsBuilder(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
        // Use supportFragmentManager in an Activity or requireActivity().supportFragmentManager in a Fragment.
        .runtimeHandlerProvider(CustomRuntimePermissionHandlerProvider(supportFragmentManager))
        .build()
        .sendSuspend()
    if (result.allGranted()) { ... }
}

@GuilhE I'll try to reproduce it, thanks.

In the meanwhile, can you try to call in your Application the method useLegacyRuntimePermissionHandler() using the version 3.2.0 and tell me if it fixes your problem?

Yup adding that line solves the problem

@GuilhE Perfect, when you have time, can you do the other try I suggested? If that second try works, I'll release a fix soon

@GuilhE Perfect, when you have time, can you do the other try I suggested? If that second try works, I'll release a fix soon

Already doing it ๐Ÿ˜‰
Btw, I have other permissions (RECORD_AUDIO and CAMERA) and I don't have this problem with them ๐Ÿค”

Using CustomRuntimePermissionHandlerProvider doesn't solve.

Btw, I have other permissions (for Camera and Gallery) and I don't have this problem with them, maybe because they open other activities? ๐Ÿค”

Are you using launchWithResumed also to send those requests or you are sending them after an event (e.g. a user click)?
Maybe that's the difference between them.

Using CustomRuntimePermissionHandlerProvider doesn't solve.

This is strange, I expected that it would have solved the problem.
I've tried to reproduce your same behavior in the sample of this library and I hadn't any problem with launchWhenResumed but I had it with launchWhenCreated and using the class I pasted above should have solved the problem.

I'll investigate more about it.
In case I won't be able to reproduce it, would you be able to provide a sample in which the same error happens? (even using the sample provided by this library if you want)

Until this problem is not solved, you can use useLegacyRuntimePermissionHandler() which opts-in the behavior of the version 3.1.3.

Are you using launchWithResumed also to send those requests or you are sending them after an event (e.g. a user click)?
Maybe that's the difference between them.

Yes I forgot, the use case is quite different, for CAMERA and RECORD_AUDIO is upon user interaction (click). For the location it's "onCreate".

Unfortunately I'm not able to provide access to the code but it's quite simple I'll explain the use case. I've an Activity with a ViewPager. Only 2 pages are being created (Fragments) the first is a List of locations and the second a Map with markers.

MapFragment:

override fun onMapReady(map: GoogleMap) {
        with(map) {
            ...
        lifecycleScope.launchWhenResumed {
            val result = permissionsBuilder(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
                .runtimeHandlerProvider(CustomRuntimePermissionHandlerProvider(requireActivity().supportFragmentManager))
                .build()
                .sendSuspend()

            if (result.allGranted()) {
                map.isMyLocationEnabled = true
                ...
            }
            ...    
        }
    }
}

ListFragment:

override fun initViews(binding: FragmentListBinding, viewModel: ViewModel) {
        ...
        lifecycleScope.launchWhenResumed {
            val result = permissionsBuilder(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
                .runtimeHandlerProvider(CustomRuntimePermissionHandlerProvider(requireActivity().supportFragmentManager))
                .build()
                .sendSuspend()

            if (result.allGranted()) {
                ...
            } 
        }
}

I'll investigate more about it.

Feel free to ask for any help with testing ๐Ÿ˜‰

I'll try to reproduce a similar scenario, in case I'll ask you for some help on this one, thanks!

I tried with a similar configuration as yours but unfortunately I couldn't reproduce the problem yet.
Can you add the following logs and tell me how and if they are printed when you grant the permissions?

Right before lifecycleScope.launchWhenResumed in the map fragment:

Log.d("KPermissions", "before launchWhenResumed - map")

Right before lifecycleScope.launchWhenResumed in the list fragment:

Log.d("KPermissions", "before launchWhenResumed - list")

After sendSuspend in the map fragment:

Log.d("KPermissions", "after sendSuspend - map")

After sendSuspend in the list fragment:

Log.d("KPermissions", "after sendSuspend - list")

This because I see few problems here:

  1. KPermissions allows max 1 permission request at the same time, in the sample you have provided, you are sending two different requests at the same time, so one of them shouldn't receive the result
  2. You are using lifecycleScope instead of viewLifecycleOwner.lifecycleOwner in a fragment

This is what's printed when I use useLegacyRuntimePermissionHandler() regardless of my choice:

2021-03-18 09:24:48.419 D/KPermissions: before launchWhenResumed - list
2021-03-18 09:24:48.428 D/KPermissions: after sendSuspend - list
2021-03-18 09:24:49.084 D/KPermissions: before lifecycleScope.launchWhenResumed - map
2021-03-18 09:24:57.239 D/KPermissions: after sendSuspend - map

note: after sendSuspend - map is fired only when I switch page, and in this case I granted the permissions in the first page.
note 2: the log only gets printed after I made a choice ๐Ÿค”

When I don't use useLegacyRuntimePermissionHandler() nothing gets printed until I go back and forth (like expalined above). Except when I deny permissions (forever). Then it will never reach if (result.allGranted())until I go to app settings / permissions and change it.

KPermissions allows max 1 permission request at the same time, in the sample you have provided, you are sending two different requests at the same time, so one of them shouldn't receive the result

When you say "1 permission request at the same time" you mean I can't call .sendSuspend() simultaneous right?
Well it worked without any problem so far because as you can see by the log, .sendSuspend() aren't called simultaneous.

You are using lifecycleScope instead of viewLifecycleOwner.lifecycleOwner in a fragment

Nice catch! My observables are using viewLifecycleOwner.lifecycleOwner, I missed this one. Changed it but the behaviour didn't change.

Both of these sound strange.

note 2: the log only gets printed after I made a choice ๐Ÿค”

When I don't use useLegacyRuntimePermissionHandler() nothing gets printed until I go back and forth (like expalined above).

Just to be sure I explained it right before, I expect the logs to be in this position:

override fun initViews(binding: FragmentListBinding, viewModel: ViewModel) {
    Log.d("KPermissions", "before launchWhenResumed - list")
    lifecycleScope.launchWhenResumed {
        val result = permissionsBuilder(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
            .runtimeHandlerProvider(CustomRuntimePermissionHandlerProvider(requireActivity().supportFragmentManager))
            .build()
            .sendSuspend()
        Log.d("KPermissions", "after sendSuspend - list")
        if (result.allGranted()) {
            ...
        } 
    }
}

When you say "1 permission request at the same time" you mean I can't call .sendSuspend() simultaneous right?
Well it worked without any problem so far because as you can see by the log, .sendSuspend() aren't called simultaneous.

You are right, only one of the two fragments is being resumed so the requests aren't simultaneous, so both the requests should deliver a result when launched.

Yup, a strange behaviour indeed.
Yes the logs were positioned as you asked but I had the CustomRuntimePermissionHandlerProvider removed. Should I try again with it?

This library doesn't change anything in the container (activity or fragment) before the first call to permissionsBuilder() is invoked.
Are you creating a permission request before your calls to launchWhenResumed? Or you are maybe creating the request outside it and sending it inside it?

Yes the logs were positioned as you asked but I had the CustomRuntimePermissionHandlerProvider removed. Should I try again with it?

It shouldn't make any difference.

I'm creating and calling inside viewLifecycleOwner.lifecycleScope.launchWhenResume as follow:

override fun initViews(binding: FragmentListBinding, viewModel: ViewModel) {
        with(binding.myRecyclerView) {
            ...
        }
        viewModel.someData.observe(viewLifecycleOwner, {
            ...
        })
        viewLifecycleOwner.lifecycleScope.launchWhenResumed {
            val result = permissionsBuilder(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION)
                .build()
                .sendSuspend()
            if (result.allGranted()) {
                //fetch data and populate UI
            } else showWarning()
        }
}

There should be something else which impacts your code, because this library doesn't do absolutely anything before your first call to permissionsBuilder().

I guess we have two solutions here since I can't reproduce it:

  1. you can try to provide a sample in which you can reproduce the bug
  2. we need to "debug" it together since there's something strange happening

For the second solution, we can start trying to put a breakpoint inside the extensions permissionsBuilder() of this library. You can see where is it called from. Is it called from the list fragment, from the map fragment or from a 3rd caller we don't know yet?

Well I didn't change this piece of code for a long time and it was working, only thing I was doing was updating some libs (yours inclusive). Can it be a combination with a google lib that's causing this?

const val activity_ktx: String = "1.2.0" > "1.2.1"
const val fragment_ktx: String = "1.3.0" > "1.3.1"
const val kpermissions: String = "3.1.3" > "3.2.0"

Last time I've updated kpermission was in ~20/11/2020 since then a few updates from activity_ktx and fragment_ktx were released ๐Ÿค”

I'll try to downgrade and test.

First, we can ensure KPermissions is involved with the debug try I wrote above.
I expect that method to be called.
If it's not called, something else is happening.
I expect this to be a bug in my lib since using useLegacyRuntimePermissionHandler() fixes the problem.

I think it could be called somewhere else before your the snippets you posted from your list and map fragments.

There should be something else which impacts your code, because this library doesn't do absolutely anything before your first call to permissionsBuilder().

I guess we have two solutions here since I can't reproduce it:

1. you can try to provide a sample in which you can reproduce the bug

2. we need to "debug" it together since there's something strange happening

For the second solution, we can start trying to put a breakpoint inside the extensions permissionsBuilder() of this library. You can see where is it called from. Is it called from the list fragment, from the map fragment or from a 3rd caller we don't know yet?

Ok so here it goes:

  1. ListFragment calls .sendSuspend()
  2. permission builder uses ResultLauncherRuntimePermissionHandlerProvider(supportFragmentManager)
  3. ResultLauncherRuntimePermissionHandlerProvider creates Fragment delegated to handle permissions
  4. SuspendExtensions calls PermissionRequest.sendSuspend()
  5. ResultLauncherRuntimePermissionHandler.handleRuntimePermissions calls pendingHandleRuntimePermissions because isAdded = false
  6. Permission dialog is shown and I choose Allow while using app.
  7. ResultLauncherRuntimePermissionHandler.onPermissionsResult it's called but the breakpoint at line 98 and 105 doesn't get called.
  8. I go back and forth, everything runs the same but now PermissionRequest.Listener.onPermissionsResult it's called with result
  9. And (result.allGranted()) is reached

So I guess this is the line to blame: val pendingPermissions = pendingPermissions ?: return

@VisibleForTesting
   fun onPermissionsResult(permissionsResult: Map<String, Boolean>) {
       val pendingPermissions = pendingPermissions ?: return
       // Now the Fragment is not processing the permissions anymore.
       this.pendingPermissions = null

shouldn't you be querying permissionsResult? ๐Ÿค”

Second time works because of this:

private fun handleRuntimePermissionsWhenAdded(permissions: Array<out String>) {
        // Get the listener for this set of permissions.
        // If it's null, the permissions can't be notified.
        val listener = listeners[permissions.toSet()] ?: return
        val activity = requireActivity()
        val currentStatus = activity.checkRuntimePermissionsStatus(permissions.toList())  <-------
        val areAllGranted = currentStatus.allGranted()  <-------
        if (!areAllGranted) {
            if (pendingPermissions != null) {
                // The Fragment can process only one request at the same time.
                return
            }
            // Request the permissions.
            @Suppress("DEPRECATION")
            requestRuntimePermissions(permissions)
        } else {
            listener.onPermissionsResult(currentStatus) <-------
        }
    }

Mystery solved ๐Ÿ˜ƒ

Thanks for the deep debugging, this indeed helps.

shouldn't you be querying permissionsResult? ๐Ÿค”

I do after, but pendingPermissions was needed because there was a bug in AndroidX Fragment 1.3.0 for which some permissions weren't contained in the result.

So, I guess that pendingPermissions is null. Can you verify when that class makes again pendingPermissions as null? I guess it could be in the method onCreate().

In the meanwhile, I'll try to see if the bug is completely solved with the new version of AndroidX Fragment 1.3.1.

So, I guess that pendingPermissions is null. Can you verify when that class makes again pendingPermissions as null? I guess it could be in the method onCreate().

Exactly ๐Ÿ‘Œ

Strange it wasn't solved with the provider I added above, because that was the bug I fixed.

Can you try with the following one?

import android.app.Activity
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import com.fondesa.kpermissions.PermissionStatus
import com.fondesa.kpermissions.allGranted
import com.fondesa.kpermissions.extension.isPermissionGranted
import com.fondesa.kpermissions.request.runtime.RuntimePermissionHandler
import com.fondesa.kpermissions.request.runtime.RuntimePermissionHandlerProvider

internal class CustomRuntimePermissionHandlerProvider(private val manager: FragmentManager) : RuntimePermissionHandlerProvider {
    @RequiresApi(23)
    override fun provideHandler(): RuntimePermissionHandler {
        var fragment = manager.findFragmentByTag(FRAGMENT_TAG) as? RuntimePermissionHandler
        if (fragment == null) {
            fragment = CustomRuntimePermissionsHandler()
            manager.beginTransaction()
                .add(fragment, FRAGMENT_TAG)
                .commitAllowingStateLoss()
        }
        return fragment
    }

    @RequiresApi(23)
    class CustomRuntimePermissionsHandler : Fragment(), RuntimePermissionHandler {
        private val resultLauncher = registerForActivityResult(
            ActivityResultContracts.RequestMultiplePermissions(),
            ::onPermissionsResult
        )
        private val listeners = mutableMapOf<Set<String>, RuntimePermissionHandler.Listener>()
        private var pendingHandleRuntimePermissions: (() -> Unit)? = null
        private var isProcessingPermissions = false

        override fun onAttach(context: Context) {
            super.onAttach(context)
            pendingHandleRuntimePermissions?.invoke()
            pendingHandleRuntimePermissions = null
        }

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            isProcessingPermissions = savedInstanceState?.getBoolean(KEY_IS_PROCESSING_PERMISSIONS) ?: false
        }

        override fun onSaveInstanceState(outState: Bundle) {
            super.onSaveInstanceState(outState)
            outState.putBoolean(KEY_IS_PROCESSING_PERMISSIONS, isProcessingPermissions)
        }

        override fun attachListener(permissions: Array<out String>, listener: RuntimePermissionHandler.Listener) {
            listeners[permissions.toSet()] = listener
        }

        override fun handleRuntimePermissions(permissions: Array<out String>) {
            if (isAdded) {
                handleRuntimePermissionsWhenAdded(permissions)
            } else {
                pendingHandleRuntimePermissions = { handleRuntimePermissionsWhenAdded(permissions) }
            }
        }

        @Suppress("OverridingDeprecatedMember")
        override fun requestRuntimePermissions(permissions: Array<out String>) {
            isProcessingPermissions = true
            Log.d(TAG, "requesting permissions: ${permissions.joinToString()}")
            resultLauncher.launch(permissions)
        }

        private fun handleRuntimePermissionsWhenAdded(permissions: Array<out String>) {
            val listener = listeners[permissions.toSet()] ?: return
            val activity = requireActivity()
            val currentStatus = activity.checkRuntimePermissionsStatus(permissions.toList())
            val areAllGranted = currentStatus.allGranted()
            if (!areAllGranted) {
                if (isProcessingPermissions) return
                @Suppress("DEPRECATION")
                requestRuntimePermissions(permissions)
            } else {
                listener.onPermissionsResult(currentStatus)
            }
        }

        private fun onPermissionsResult(permissionsResult: Map<String, Boolean>) {
            isProcessingPermissions = false
            val listener = listeners[permissionsResult.keys] ?: return
            val result = permissionsResult.map { (permission, isGranted) ->
                when {
                    isGranted -> PermissionStatus.Granted(permission)
                    shouldShowRequestPermissionRationale(permission) -> PermissionStatus.Denied.ShouldShowRationale(permission)
                    else -> PermissionStatus.Denied.Permanently(permission)
                }
            }
            listener.onPermissionsResult(result)
        }

        private fun Activity.checkRuntimePermissionsStatus(permissions: List<String>): List<PermissionStatus> =
            permissions.map { permission ->
                if (isPermissionGranted(permission)) {
                    return@map PermissionStatus.Granted(permission)
                }
                if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) {
                    PermissionStatus.Denied.ShouldShowRationale(permission)
                } else {
                    PermissionStatus.RequestRequired(permission)
                }
            }

        companion object {
            private val TAG = CustomRuntimePermissionHandlerProvider::class.java.simpleName
            private const val KEY_IS_PROCESSING_PERMISSIONS = "is_processing_permissions"
        }
    }

    companion object {
        private const val FRAGMENT_TAG = "CustomPermissionsFragment"
    }
}

It works!

Perfect!! Thanks!

Can you still try using the first CustomRuntimePermissionHandlerProvider telling me what happens there? Because that should have solved the bug of pendingPermissions reset in onCreate()

It also works.
But now I realise we had a false positive in the first time because while I was testing I moved the permission to the Activity that hosts both fragments and then I forgot to remove the code from the Activity. Note that I wasn't calling permission twice because the fragments weren't being added since (result.allGranted()) in the Activity wasn't being called either.
So, If I leave the code in the Activity both CustomRuntimePermissionHandlerProvider won't work, but if move that logic to the fragments initViews, it works as expected (with both).

Note that if I downgrade to 3.1.3 or use useLegacyRuntimePermissionHandler() it works with the code also in the Activity.

but if move that logic to the fragments initViews, it works as expected (with both).

Perfect, thanks. I'm fixing this and release a new version after.

So, If I leave the code in the Activity both CustomRuntimePermissionHandlerProvider won't work

So, now you have fragment list, fragment map and their activity.

  1. Where did you sent the permission request? Only in the activity or in all the 3 places?
  2. Did you use the CustomRuntimePermissionHandlerProvider in all these 3 places?
  3. How do you send the request in the activity? With lifecycleScope.launch { } in onCreate() or how?

Sorry if this issue is becoming a bit painful but I don't want to ship a partially-working version

So, If I leave the code in the Activity both CustomRuntimePermissionHandlerProvider won't work

So, now you have fragment list, fragment map and their activity.

1. Where did you sent the permission request? Only in the activity or in all the 3 places?

Only in the Activity, because the Permission Dialog shows up and the ListFragment wouldn't reach onResume state so launchWhenResumed wouldn't launch.

2. Did you use the `CustomRuntimePermissionHandlerProvider` in all these 3 places?

Yes except in the Activity (because I forgot to remove the code).
But now that you asked I've added .runtimeHandlerProvider(CustomRuntimePermissionHandlerProvider(supportFragmentManager)) in the Activity and it worked.

3. How do you send the request in the activity? With `lifecycleScope.launch { }` in `onCreate()` or how?

lifecycleScope.launchWhenResumed same logic as in Fragments

Sorry if this issue is becoming a bit painful but I don't want to ship a partially-working version

Of course, no problem ๐Ÿ‘
Your CustomRuntimePermissionHandlerProvider solves this problem.

Perfect, thanks for your great help for this issue, I wouldn't be able to spot and fix this issue alone.

I'll release a fix soon. Until that moment I'm leaving this issue open also for the other users.

I'm the one who thanks all your effort to provide us with this awesome lib. The least I can do is to help :)

@GuilhE Can you try the version 3.2.1 and check that the bug is fixed?

It's fixed!