@@ -17,81 +17,149 @@ import androidx.lifecycle.Lifecycle
1717import androidx.lifecycle.LifecycleEventObserver
1818import kotlinx.coroutines.CompletableDeferred
1919import kotlinx.coroutines.Dispatchers
20+ import kotlinx.coroutines.TimeoutCancellationException
2021import kotlinx.coroutines.suspendCancellableCoroutine
2122import kotlinx.coroutines.sync.Mutex
2223import kotlinx.coroutines.sync.withLock
2324import kotlinx.coroutines.withContext
25+ import kotlinx.coroutines.withTimeout
2426import java.util.concurrent.atomic.AtomicBoolean
2527import kotlin.coroutines.resume
2628
27- class PermissionRequester (
29+ class PermissionRequester internal constructor (
2830 private val activity : ComponentActivity ,
31+ launcherFactory : ((Map <String , Boolean >) -> Unit ) -> ActivityResultLauncher <Array <String >>,
2932) {
33+ private data class PendingPermissionRequest (
34+ val deferred : CompletableDeferred <Map <String , Boolean >>,
35+ var timedOut : Boolean = false ,
36+ )
37+
38+ private class PermissionRequestSlot (
39+ val launcher : ActivityResultLauncher <Array <String >>,
40+ var request : PendingPermissionRequest ? = null ,
41+ )
42+
43+ constructor (activity: ComponentActivity ) : this (
44+ activity = activity,
45+ launcherFactory = { callback ->
46+ activity.registerForActivityResult(ActivityResultContracts .RequestMultiplePermissions (), callback)
47+ },
48+ )
49+
3050 private val mutex = Mutex ()
31- private var pending : CompletableDeferred < Map < String , Boolean >> ? = null
51+ private val requestSlotsLock = Any ()
3252 private val mainHandler = Handler (Looper .getMainLooper())
33-
34- private val launcher: ActivityResultLauncher <Array <String >> =
35- activity.registerForActivityResult(ActivityResultContracts .RequestMultiplePermissions ()) { result ->
36- val p = pending
37- pending = null
38- p?.complete(result)
39- }
53+ private val launchers = List (4 ) { createPermissionRequestSlot(launcherFactory) }
4054
4155 suspend fun requestIfMissing (
4256 permissions : List <String >,
4357 timeoutMs : Long = 20_000,
44- ): Map <String , Boolean > =
45- mutex.withLock {
46- val missing =
47- permissions.filter { perm ->
48- ContextCompat .checkSelfPermission(activity, perm) != PackageManager .PERMISSION_GRANTED
58+ ): Map <String , Boolean > {
59+ return mutex.withLock {
60+ while (true ) {
61+ val missing =
62+ permissions.filter { perm ->
63+ ContextCompat .checkSelfPermission(activity, perm) != PackageManager .PERMISSION_GRANTED
64+ }
65+ if (missing.isEmpty()) {
66+ return permissions.associateWith { true }
4967 }
50- if (missing.isEmpty()) {
51- return permissions.associateWith { true }
52- }
5368
54- val needsRationale =
55- missing.any { ActivityCompat .shouldShowRequestPermissionRationale(activity, it) }
56- if (needsRationale) {
57- val proceed = showRationaleDialog(missing)
58- if (! proceed) {
59- return permissions.associateWith { perm ->
60- ContextCompat .checkSelfPermission(activity, perm) == PackageManager .PERMISSION_GRANTED
69+ val needsRationale =
70+ missing.any { ActivityCompat .shouldShowRequestPermissionRationale(activity, it) }
71+ if (needsRationale) {
72+ val proceed = showRationaleDialog(missing)
73+ if (! proceed) {
74+ return permissions.associateWith { perm ->
75+ ContextCompat .checkSelfPermission(activity, perm) == PackageManager .PERMISSION_GRANTED
76+ }
6177 }
6278 }
63- }
64-
65- val deferred = CompletableDeferred <Map <String , Boolean >>()
66- pending = deferred
67- withContext(Dispatchers .Main ) {
68- launcher.launch(missing.toTypedArray())
69- }
7079
71- val result =
72- withContext(Dispatchers .Default ) {
73- kotlinx.coroutines.withTimeout(timeoutMs) { deferred.await() }
80+ val deferred = CompletableDeferred <Map <String , Boolean >>()
81+ val request = PendingPermissionRequest (deferred)
82+ val slot = reservePermissionRequestSlot(request)
83+ try {
84+ withContext(Dispatchers .Main ) {
85+ slot.launcher.launch(missing.toTypedArray())
86+ }
87+ } catch (err: Throwable ) {
88+ clearPermissionRequestSlot(slot, request)
89+ throw err
7490 }
7591
76- // Merge: if something was already granted, treat it as granted even if launcher omitted it.
77- val merged =
78- permissions.associateWith { perm ->
79- val nowGranted =
80- ContextCompat .checkSelfPermission(activity, perm) == PackageManager .PERMISSION_GRANTED
81- result[perm] == true || nowGranted
82- }
92+ val result =
93+ try {
94+ withTimeout(timeoutMs) { deferred.await() }
95+ } catch (err: TimeoutCancellationException ) {
96+ request.timedOut = true
97+ throw err
98+ }
99+
100+ val merged =
101+ permissions.associateWith { perm ->
102+ val nowGranted =
103+ ContextCompat .checkSelfPermission(activity, perm) == PackageManager .PERMISSION_GRANTED
104+ result[perm] == true || nowGranted
105+ }
83106
84- val denied =
85- merged.filterValues { ! it }.keys.filter {
86- ! ActivityCompat .shouldShowRequestPermissionRationale(activity, it)
107+ val denied =
108+ merged.filterValues { ! it }.keys.filter {
109+ ! ActivityCompat .shouldShowRequestPermissionRationale(activity, it)
110+ }
111+ if (denied.isNotEmpty()) {
112+ showSettingsDialog(denied)
87113 }
88- if (denied.isNotEmpty()) {
89- showSettingsDialog(denied)
114+
115+ return merged
90116 }
117+ error(" unreachable" )
118+ }
119+ }
120+
121+ private fun createPermissionRequestSlot (
122+ launcherFactory : ((Map <String , Boolean >) -> Unit ) -> ActivityResultLauncher <Array <String >>,
123+ ): PermissionRequestSlot {
124+ var slot: PermissionRequestSlot ? = null
125+ val launcher = launcherFactory { result -> completePermissionRequest(checkNotNull(slot), result) }
126+ val created = PermissionRequestSlot (launcher)
127+ slot = created
128+ return created
129+ }
91130
92- return merged
131+ private fun reservePermissionRequestSlot (request : PendingPermissionRequest ): PermissionRequestSlot =
132+ synchronized(requestSlotsLock) {
133+ val slot = launchers.firstOrNull { it.request == null } ? : error(" permission request launcher busy" )
134+ slot.request = request
135+ slot
93136 }
94137
138+ private fun completePermissionRequest (
139+ slot : PermissionRequestSlot ,
140+ result : Map <String , Boolean >,
141+ ) {
142+ val request =
143+ synchronized(requestSlotsLock) {
144+ slot.request.also {
145+ slot.request = null
146+ }
147+ } ? : return
148+ if (request.timedOut) return
149+ request.deferred.complete(result)
150+ }
151+
152+ private fun clearPermissionRequestSlot (
153+ slot : PermissionRequestSlot ,
154+ request : PendingPermissionRequest ,
155+ ) {
156+ synchronized(requestSlotsLock) {
157+ if (slot.request == = request) {
158+ slot.request = null
159+ }
160+ }
161+ }
162+
95163 private suspend fun showRationaleDialog (permissions : List <String >): Boolean =
96164 withContext(Dispatchers .Main ) {
97165 if (activity.isFinishing || activity.isDestroyed) {
0 commit comments