Coverage Summary for Class: PermissionFlowProcessor (kr.open.library.simple_ui.xml.permissions.flow)
| Class |
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| PermissionFlowProcessor |
64.3%
(18/28)
|
36.8%
(56/152)
|
54.5%
(144/264)
|
55.9%
(817/1461)
|
| PermissionFlowProcessor$awaitHostStarted$2$1 |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/6)
|
| PermissionFlowProcessor$awaitHostStarted$2$observer$1 |
0%
(0/3)
|
0%
(0/4)
|
0%
(0/7)
|
0%
(0/38)
|
| PermissionFlowProcessor$awaitRationaleDecision$2$1 |
75%
(3/4)
|
|
80%
(4/5)
|
80%
(16/20)
|
| PermissionFlowProcessor$awaitSettingsDecision$2$1 |
75%
(3/4)
|
|
80%
(4/5)
|
80%
(16/20)
|
| PermissionFlowProcessor$awaitUserDecision$2$1 |
100%
(3/3)
|
100%
(2/2)
|
100%
(4/4)
|
100%
(27/27)
|
| PermissionFlowProcessor$awaitUserDecision$2$2 |
0%
(0/1)
|
0%
(0/2)
|
0%
(0/1)
|
0%
(0/16)
|
| PermissionFlowProcessor$awaitUserDecision$2$dispatched$1$1 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(10/10)
|
| PermissionFlowProcessor$awaitUserDecision$2$dispatched$1$2 |
100%
(1/1)
|
|
100%
(1/1)
|
100%
(10/10)
|
| PermissionFlowProcessor$awaitUserDecision$2$dispatched$1$3 |
100%
(1/1)
|
100%
(2/2)
|
100%
(4/4)
|
100%
(22/22)
|
| PermissionFlowProcessor$awaitUserDecision$2$dispatched$1$3$WhenMappings |
|
| PermissionFlowProcessor$InFlightActivityAction |
0%
(0/1)
|
|
0%
(0/1)
|
0%
(0/2)
|
| PermissionFlowProcessor$InFlightActivityAction$Role |
0%
(0/1)
|
|
0%
(0/2)
|
0%
(0/8)
|
| PermissionFlowProcessor$InFlightActivityAction$Special |
0%
(0/1)
|
|
0%
(0/2)
|
0%
(0/8)
|
| PermissionFlowProcessor$PermissionDecisionState |
100%
(1/1)
|
|
100%
(4/4)
|
100%
(30/30)
|
| PermissionFlowProcessor$process$1 |
|
| PermissionFlowProcessor$processRequest$1 |
|
| PermissionFlowProcessor$processRolePermission$1 |
|
| PermissionFlowProcessor$processRuntimePermissions$1 |
|
| PermissionFlowProcessor$processSpecialPermission$1 |
|
| PermissionFlowProcessor$resumeObserver$1 |
100%
(3/3)
|
33.3%
(2/6)
|
60%
(3/5)
|
47.6%
(10/21)
|
| PermissionFlowProcessor$WhenMappings |
|
| Total |
63%
(34/54)
|
36.9%
(62/168)
|
55%
(169/307)
|
56.4%
(958/1699)
|
package kr.open.library.simple_ui.xml.permissions.flow
import android.content.Intent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.suspendCancellableCoroutine
import kr.open.library.simple_ui.core.extensions.trycatch.safeCatch
import kr.open.library.simple_ui.core.logcat.Logx
import kr.open.library.simple_ui.core.permissions.classifier.PermissionClassifier
import kr.open.library.simple_ui.core.permissions.classifier.PermissionType
import kr.open.library.simple_ui.core.permissions.classifier.RuntimePermissionRequestability
import kr.open.library.simple_ui.core.permissions.extensions.hasPermission
import kr.open.library.simple_ui.core.permissions.handler.RolePermissionHandler
import kr.open.library.simple_ui.core.permissions.handler.SpecialPermissionHandler
import kr.open.library.simple_ui.core.permissions.model.PermissionDecisionType
import kr.open.library.simple_ui.core.permissions.model.PermissionDeferredPolicy
import kr.open.library.simple_ui.core.permissions.model.PermissionRationaleRequest
import kr.open.library.simple_ui.core.permissions.model.PermissionSettingsRequest
import kr.open.library.simple_ui.xml.permissions.coordinator.RequestEntry
import kr.open.library.simple_ui.xml.permissions.host.PermissionHostAdapter
import kr.open.library.simple_ui.xml.permissions.result.PermissionResultAggregator
import kotlin.coroutines.resume
/**
* Processes runtime, special, and role permission flows.<br><br>
* 런타임/특수/Role 권한 흐름을 처리합니다.<br>
*
* @param host Host adapter supplying Activity/Fragment capabilities.<br><br>
* Activity/Fragment 기능을 제공하는 호스트 어댑터입니다.<br>
* @param classifierProvider Permission classifier provider for runtime/special/role decisions.<br><br>
* 런타임/특수/Role 판단에 사용하는 권한 분류기 provider입니다.<br>
* @param runtimeHandler Handler for runtime permission rationale and mapping.<br><br>
* 런타임 권한 설명/결과 매핑 처리기입니다.<br>
* @param specialHandlerProvider Handler provider for special permission checks and intents.<br><br>
* 특수 권한 확인/인텐트 처리기 provider입니다.<br>
* @param roleHandlerProvider Handler provider for role availability and intents.<br><br>
* Role 사용 가능 여부/인텐트 처리기 provider입니다.<br>
* @param resultAggregatorProvider Aggregator provider for result updates and completion.<br><br>
* 결과 갱신/완료 처리를 담당하는 집계기 provider입니다.<br>
*/
internal class PermissionFlowProcessor(
private val host: PermissionHostAdapter,
private val classifierProvider: () -> PermissionClassifier,
private val runtimeHandler: RuntimePermissionHandler,
private val specialHandlerProvider: () -> SpecialPermissionHandler,
private val roleHandlerProvider: () -> RolePermissionHandler,
private val resultAggregatorProvider: () -> PermissionResultAggregator,
) {
/**
* Lazily resolves the classifier to avoid early context access during requester construction.<br><br>
* 요청기 생성 시 조기 context 접근을 피하기 위해 분류기를 지연 조회합니다.<br>
*/
private val classifier: PermissionClassifier
get() = classifierProvider()
/**
* Lazily resolves the special permission handler.<br><br>
* 특수 권한 처리기를 지연 조회합니다.<br>
*/
private val specialHandler: SpecialPermissionHandler
get() = specialHandlerProvider()
/**
* Lazily resolves the role permission handler.<br><br>
* Role 권한 처리기를 지연 조회합니다.<br>
*/
private val roleHandler: RolePermissionHandler
get() = roleHandlerProvider()
/**
* Lazily resolves the result aggregator.<br><br>
* 결과 집계기를 지연 조회합니다.<br>
*/
private val resultAggregator: PermissionResultAggregator
get() = resultAggregatorProvider()
/**
* Snapshot of "requested before" flags for the current runtime request.<br><br>
* 현재 런타임 요청에 대한 이전 요청 여부 스냅샷입니다.<br>
*/
private var runtimeRequestedBefore: Map<String, Boolean> = emptyMap()
/**
* Tracks the currently launched settings/role activity action.<br><br>
* 현재 실행 중인 설정/Role 액션을 추적합니다.<br>
*/
private var currentActivityAction: InFlightActivityAction? = null
/**
* Flag indicating a special permission result should be checked on resume.<br><br>
* onResume 시 특수 권한 결과 재확인이 필요한지 나타내는 플래그입니다.<br>
*/
private var awaitingSpecialReturn: Boolean = false
/**
* Flag indicating the host actually left the screen for special permission settings.<br><br>
* 특수 권한 설정 화면으로 실제 이탈했는지 여부를 나타내는 플래그입니다.<br>
*/
private var hasLeftForSpecial: Boolean = false
/**
* Deferred completion for runtime permission requests.<br><br>
* 런타임 권한 요청 완료를 기다리는 디퍼드입니다.<br>
*/
private var runtimeCompletion: CompletableDeferred<Unit>? = null
/**
* Deferred completion for special permission requests.<br><br>
* 특수 권한 요청 완료를 기다리는 디퍼드입니다.<br>
*/
private var specialCompletion: CompletableDeferred<Unit>? = null
/**
* Deferred completion for role permission requests.<br><br>
* Role 권한 요청 완료를 기다리는 디퍼드입니다.<br>
*/
private var roleCompletion: CompletableDeferred<Unit>? = null
/**
* Lifecycle observer that rechecks special permissions on resume.<br><br>
* onResume 시 특수 권한을 재확인하는 라이프사이클 옵저버입니다.<br>
*/
private val resumeObserver: DefaultLifecycleObserver = object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
if (awaitingSpecialReturn && hasLeftForSpecial) {
handleSpecialPermissionReturn()
}
}
override fun onStop(owner: LifecycleOwner) {
if (awaitingSpecialReturn) {
hasLeftForSpecial = true
}
}
}
/**
* ActivityResult launcher for runtime permission dialogs.<br><br>
* 런타임 권한 다이얼로그 실행용 ActivityResult 런처입니다.<br>
*/
private val runtimePermissionsLauncher: ActivityResultLauncher<Array<String>> =
host.activityResultCaller.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { results ->
handleRuntimePermissionsResult(results)
}
/**
* ActivityResult launcher for settings/role intents.<br><br>
* 설정/Role 인텐트 실행용 ActivityResult 런처입니다.<br>
*/
private val activityLauncher: ActivityResultLauncher<Intent> =
host.activityResultCaller.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
handleActivityResultReturn()
}
/**
* Initializes lifecycle observers for special permission returns.<br><br>
* 특수 권한 복귀 처리를 위한 라이프사이클 옵저버를 초기화합니다.<br>
*/
init {
host.lifecycleOwner.lifecycle.addObserver(resumeObserver)
}
/**
* Returns whether the lifecycle is ready to accept new requests.<br><br>
* 라이프사이클이 새 요청을 수용할 준비가 되었는지 반환합니다.<br>
*
* @return Return value: true when lifecycle is at least CREATED. Log behavior: none.<br><br>
* 반환값: CREATED 이상이면 true입니다. 로그 동작: 없음.<br>
*/
fun isLifecycleRequestAllowed(): Boolean = host.lifecycleOwner.lifecycle.currentState
.isAtLeast(Lifecycle.State.CREATED)
/**
* Processes runtime, special, and role permissions for [entry].<br><br>
* [entry]의 런타임/특수/Role 권한을 순차 처리합니다.<br>
*
* @param entry Request entry to process.<br><br>
* 처리할 요청 엔트리입니다.<br>
*/
suspend fun process(entry: RequestEntry) {
if (entry.isCompleted()) {
resultAggregator.tryCompleteRequest(entry.requestId)
return
}
processRequest(entry)
resultAggregator.tryCompleteRequest(entry.requestId)
}
/**
* Handles the runtime/special/role permission pipeline for [entry].<br><br>
* [entry]의 런타임/특수/Role 권한 파이프라인을 처리합니다.<br>
*
* @param entry Request entry to process.<br><br>
* 처리할 요청 엔트리입니다.<br>
*/
private suspend fun processRequest(entry: RequestEntry) {
var pendingPermissions = entry.pendingPermissions()
if (pendingPermissions.isEmpty()) return
val runtimePermissions = pendingPermissions.filter {
classifier.classify(it) == PermissionType.RUNTIME
}
if (runtimePermissions.isNotEmpty()) {
processRuntimePermissions(entry, runtimePermissions)
}
pendingPermissions = entry.pendingPermissions()
val specialPermissions = pendingPermissions.filter {
classifier.classify(it) == PermissionType.SPECIAL
}
for (permission in specialPermissions) {
processSpecialPermission(entry, permission)
}
pendingPermissions = entry.pendingPermissions()
val rolePermissions = pendingPermissions.filter {
classifier.classify(it) == PermissionType.ROLE
}
for (permission in rolePermissions) {
processRolePermission(entry, permission)
}
}
/**
* Handles runtime permission flow for [permissions].<br><br>
* [permissions]에 대한 런타임 권한 흐름을 처리합니다.<br>
*
* @param entry Request entry owning the runtime permissions.<br><br>
* 런타임 권한을 보유한 요청 엔트리입니다.<br>
* @param permissions Runtime permissions to process.<br><br>
* 처리할 런타임 권한 목록입니다.<br>
*/
private suspend fun processRuntimePermissions(entry: RequestEntry, permissions: List<String>) {
if (permissions.isEmpty()) return
if (entry.isRestored) {
val restoredResults = permissions.associateWith { permission ->
if (!isPlatformPermissionAvailable(permission, PermissionType.RUNTIME)) {
PermissionDecisionType.GRANTED
} else {
when (classifier.getRuntimeRequestability(permission)) {
RuntimePermissionRequestability.GRANTED_BY_DEFAULT ->
PermissionDecisionType.GRANTED
RuntimePermissionRequestability.NOT_SUPPORTED ->
PermissionDecisionType.NOT_SUPPORTED
RuntimePermissionRequestability.REQUESTABLE -> {
if (host.context.hasPermission(permission)) {
PermissionDecisionType.GRANTED
} else {
val shouldShowRationale = runtimeHandler.shouldShowRationale(permission)
val wasRequestedBefore = runtimeHandler.wasRequested(permission)
runtimeHandler.mapResult(
permission = permission,
granted = false,
shouldShowRationale = shouldShowRationale,
wasRequestedBefore = wasRequestedBefore,
isRestored = true,
)
}
}
}
}
}
resultAggregator.completeWaiters(restoredResults)
return
}
val platformSupportedPermissions = permissions.filter {
isPlatformPermissionAvailable(it, PermissionType.RUNTIME)
}
val unsupportedPermissions = permissions.filterNot {
isPlatformPermissionAvailable(it, PermissionType.RUNTIME)
}
if (unsupportedPermissions.isNotEmpty()) {
resultAggregator.completeWaitersForPermissions(unsupportedPermissions, PermissionDecisionType.GRANTED)
}
val permissionsByRequestability = platformSupportedPermissions.groupBy {
classifier.getRuntimeRequestability(it)
}
val grantedByDefaultPermissions =
permissionsByRequestability[RuntimePermissionRequestability.GRANTED_BY_DEFAULT].orEmpty()
if (grantedByDefaultPermissions.isNotEmpty()) {
resultAggregator.completeWaitersForPermissions(
grantedByDefaultPermissions,
PermissionDecisionType.GRANTED,
)
}
val notSupportedPermissions =
permissionsByRequestability[RuntimePermissionRequestability.NOT_SUPPORTED].orEmpty()
if (notSupportedPermissions.isNotEmpty()) {
resultAggregator.completeWaitersForPermissions(
notSupportedPermissions,
PermissionDecisionType.NOT_SUPPORTED,
)
}
val requestablePlatformPermissions =
permissionsByRequestability[RuntimePermissionRequestability.REQUESTABLE].orEmpty()
val grantedPermissions = requestablePlatformPermissions.filter { host.context.hasPermission(it) }
if (grantedPermissions.isNotEmpty()) {
resultAggregator.completeWaitersForPermissions(grantedPermissions, PermissionDecisionType.GRANTED)
}
val requestablePermissions = requestablePlatformPermissions.filterNot {
host.context.hasPermission(it)
}
if (requestablePermissions.isEmpty()) return
val readyForLaunch = awaitHostStarted()
if (!readyForLaunch) {
resultAggregator.completeWaitersForPermissions(requestablePermissions, PermissionDecisionType.LIFECYCLE_NOT_READY)
return
}
val rationalePermissions = requestablePermissions.filter {
runtimeHandler.shouldShowRationale(it)
}
if (rationalePermissions.isNotEmpty()) {
val proceed = awaitRationaleDecision(
permissions = rationalePermissions,
onRationaleNeeded = entry.onRationaleNeeded,
)
if (!proceed) {
resultAggregator.completeWaitersForPermissions(requestablePermissions, PermissionDecisionType.DENIED)
return
}
}
runtimeRequestedBefore = requestablePermissions.associateWith {
runtimeHandler.wasRequested(it)
}
runtimeHandler.markRequested(requestablePermissions)
launchRuntimeRequest(requestablePermissions)
runtimeCompletion?.await()
}
/**
* Launches the runtime permission request dialog.<br><br>
* 런타임 권한 요청 다이얼로그를 실행합니다.<br>
*
* @param permissions Permissions to request from the system.<br><br>
* 시스템에 요청할 권한 목록입니다.<br>
*/
private fun launchRuntimeRequest(permissions: List<String>) {
if (permissions.isEmpty()) return
runtimeCompletion = CompletableDeferred()
val launched = safeCatch(defaultValue = false) {
runtimePermissionsLauncher.launch(permissions.toTypedArray())
true
}
if (!launched) {
Logx.e("Failed to launch runtime permission request.")
resultAggregator.completeWaitersForPermissions(permissions, PermissionDecisionType.DENIED)
runtimeCompletion?.complete(Unit)
runtimeCompletion = null
}
}
/**
* Handles a special permission flow for [permission].<br><br>
* [permission]에 대한 특수 권한 흐름을 처리합니다.<br>
*
* @param entry Request entry owning the special permission.<br><br>
* 특수 권한을 보유한 요청 엔트리입니다.<br>
* @param permission Special permission string to process.<br><br>
* 처리할 특수 권한 문자열입니다.<br>
*/
private suspend fun processSpecialPermission(entry: RequestEntry, permission: String) {
if (entry.isRestored) {
val result = when {
!isPlatformPermissionAvailable(permission, PermissionType.SPECIAL) -> PermissionDecisionType.NOT_SUPPORTED
specialHandler.isGranted(permission) -> PermissionDecisionType.GRANTED
else -> PermissionDecisionType.DENIED
}
resultAggregator.completeWaitersForPermissions(listOf(permission), result)
return
}
if (!isPlatformPermissionAvailable(permission, PermissionType.SPECIAL)) {
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.NOT_SUPPORTED)
return
}
if (specialHandler.isGranted(permission)) {
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.GRANTED)
return
}
val intent = specialHandler.buildSettingsIntent(permission)
if (intent == null) {
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.NOT_SUPPORTED)
return
}
val readyForLaunch = awaitHostStarted()
if (!readyForLaunch) {
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.LIFECYCLE_NOT_READY)
return
}
val proceed = awaitSettingsDecision(
permission = permission,
onNavigateToSettings = entry.onNavigateToSettings,
)
if (!proceed) {
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.DENIED)
return
}
launchSpecialPermission(permission, intent)
specialCompletion?.await()
}
/**
* Launches the settings activity for [permission].<br><br>
* [permission]에 대한 설정 화면을 실행합니다.<br>
*
* @param permission Special permission being requested.<br><br>
* 요청 중인 특수 권한입니다.<br>
* @param intent Intent pointing to the settings screen.<br><br>
* 설정 화면을 가리키는 인텐트입니다.<br>
*/
private fun launchSpecialPermission(permission: String, intent: Intent) {
specialCompletion = CompletableDeferred()
currentActivityAction = InFlightActivityAction.Special(permission)
awaitingSpecialReturn = true
hasLeftForSpecial = false
val launched = safeCatch(defaultValue = false) {
activityLauncher.launch(intent)
true
}
if (!launched) {
Logx.e("Failed to launch settings for permission=$permission")
awaitingSpecialReturn = false
hasLeftForSpecial = false
currentActivityAction = null
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.FAILED_TO_LAUNCH_SETTINGS)
specialCompletion?.complete(Unit)
specialCompletion = null
}
}
/**
* Handles a role permission flow for [permission].<br><br>
* [permission]에 대한 Role 권한 흐름을 처리합니다.<br>
*
* @param entry Request entry owning the role permission.<br><br>
* Role 권한을 보유한 요청 엔트리입니다.<br>
* @param permission Role permission string to process.<br><br>
* 처리할 Role 권한 문자열입니다.<br>
*/
private suspend fun processRolePermission(entry: RequestEntry, permission: String) {
if (entry.isRestored) {
val result = when {
!roleHandler.isRoleAvailable(permission) -> PermissionDecisionType.NOT_SUPPORTED
roleHandler.isRoleHeld(permission) -> PermissionDecisionType.GRANTED
else -> PermissionDecisionType.DENIED
}
resultAggregator.completeWaitersForPermissions(
listOf(permission),
result,
)
return
}
if (!roleHandler.isRoleAvailable(permission)) {
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.NOT_SUPPORTED)
return
}
if (roleHandler.isRoleHeld(permission)) {
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.GRANTED)
return
}
val intent = roleHandler.createRequestIntent(permission)
if (intent == null) {
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.NOT_SUPPORTED)
return
}
val readyForLaunch = awaitHostStarted()
if (!readyForLaunch) {
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.LIFECYCLE_NOT_READY)
return
}
launchRolePermission(permission, intent)
roleCompletion?.await()
}
/**
* Launches the role request activity for [permission].<br><br>
* [permission]에 대한 Role 요청 액티비티를 실행합니다.<br>
*
* @param permission Role permission being requested.<br><br>
* 요청 중인 Role 권한입니다.<br>
* @param intent Role request intent to launch.<br><br>
* 실행할 Role 요청 인텐트입니다.<br>
*/
private fun launchRolePermission(permission: String, intent: Intent) {
roleCompletion = CompletableDeferred()
currentActivityAction = InFlightActivityAction.Role(permission)
val launched = safeCatch(defaultValue = false) {
activityLauncher.launch(intent)
true
}
if (!launched) {
Logx.e("Failed to launch role request for permission=$permission")
currentActivityAction = null
resultAggregator.completeWaitersForPermissions(listOf(permission), PermissionDecisionType.FAILED_TO_LAUNCH_SETTINGS)
roleCompletion?.complete(Unit)
roleCompletion = null
}
}
/**
* Awaits the rationale decision from [onRationaleNeeded].<br><br>
* [onRationaleNeeded]의 설명 결정 결과를 대기합니다.<br>
*
* @param permissions Permissions requiring rationale UI.<br><br>
* 설명 UI가 필요한 권한 목록입니다.<br>
* @param onRationaleNeeded Callback invoked for rationale UI.<br><br>
* 설명 UI를 제공하는 콜백입니다.<br>
* @return Return value: true when proceeding, false when canceled. Log behavior: logs on callback error.<br><br>
* 반환값: 진행 시 true, 취소 시 false입니다. 로그 동작: 콜백 오류 시 로깅합니다.<br>
*/
private suspend fun awaitRationaleDecision(
permissions: List<String>,
onRationaleNeeded: ((PermissionRationaleRequest) -> Unit)?
): Boolean {
if (permissions.isEmpty()) return true
val callback = onRationaleNeeded ?: return true
return awaitUserDecision { onProceed, onCancel, onDefer ->
callback.invoke(
object : PermissionRationaleRequest {
override val permissions: List<String> = permissions
override fun proceed() {
onProceed()
}
override fun cancel() {
onCancel()
}
override fun defer(policy: PermissionDeferredPolicy) {
onDefer(policy)
}
},
)
}
}
/**
* Awaits the settings navigation decision from [onNavigateToSettings].<br><br>
* [onNavigateToSettings]의 설정 이동 결정 결과를 대기합니다.<br>
*
* @param permission Special permission requiring settings navigation.<br><br>
* 설정 이동이 필요한 특수 권한입니다.<br>
* @param onNavigateToSettings Callback invoked for settings navigation UI.<br><br>
* 설정 이동 안내 콜백입니다.<br>
* @return Return value: true when proceeding, false when canceled. Log behavior: logs on callback error.<br><br>
* 반환값: 진행 시 true, 취소 시 false입니다. 로그 동작: 콜백 오류 시 로깅합니다.<br>
*/
private suspend fun awaitSettingsDecision(
permission: String,
onNavigateToSettings: ((PermissionSettingsRequest) -> Unit)?,
): Boolean {
val callback = onNavigateToSettings ?: return true
return awaitUserDecision { onProceed, onCancel, onDefer ->
callback.invoke(
object : PermissionSettingsRequest {
override val permission: String = permission
override fun proceed() {
onProceed()
}
override fun cancel() {
onCancel()
}
override fun defer(policy: PermissionDeferredPolicy) {
onDefer(policy)
}
},
)
}
}
/**
* Awaits a one-shot user decision and auto-cancels on host destroy/coroutine cancellation.<br><br>
* 1회성 사용자 결정을 대기하고 host 종료/코루틴 취소 시 자동으로 취소 처리합니다.<br>
*
* @param showUi Callback that exposes decision actions to the caller.<br><br>
* 호출자에게 결정 액션을 노출하는 콜백입니다.<br>
* @return Return value: true when proceeding, false when canceled or host destroyed.<br><br>
* 반환값: 진행 시 true, 취소 또는 host 종료 시 false입니다.<br>
*/
private suspend fun awaitUserDecision(
showUi: (
proceed: () -> Unit,
cancel: () -> Unit,
defer: (PermissionDeferredPolicy) -> Unit,
) -> Unit,
): Boolean {
val lifecycle = host.lifecycleOwner.lifecycle
if (lifecycle.currentState == Lifecycle.State.DESTROYED) return false
return suspendCancellableCoroutine { continuation ->
var state = PermissionDecisionState.ACTIVE
lateinit var observer: DefaultLifecycleObserver
fun finish(value: Boolean) {
if (state == PermissionDecisionState.FINISHED) return
state = PermissionDecisionState.FINISHED
lifecycle.removeObserver(observer)
if (continuation.isActive) {
continuation.resume(value)
}
}
observer = object : DefaultLifecycleObserver {
override fun onStop(owner: LifecycleOwner) {
if (state == PermissionDecisionState.DEFERRED_CANCEL_ON_STOP) {
finish(false)
}
}
override fun onDestroy(owner: LifecycleOwner) {
finish(false)
}
}
lifecycle.addObserver(observer)
val dispatched = safeCatch(defaultValue = false) {
showUi(
{ finish(true) },
{ finish(false) },
{ policy ->
if (state != PermissionDecisionState.FINISHED) {
state = when (policy) {
PermissionDeferredPolicy.CANCEL_ON_STOP ->
PermissionDecisionState.DEFERRED_CANCEL_ON_STOP
PermissionDeferredPolicy.CANCEL_ON_DESTROY ->
PermissionDecisionState.DEFERRED_CANCEL_ON_DESTROY
}
}
},
)
true
}
if (!dispatched) {
finish(false)
} else if (state == PermissionDecisionState.ACTIVE) {
finish(false)
}
continuation.invokeOnCancellation {
lifecycle.removeObserver(observer)
}
}
}
private enum class PermissionDecisionState {
ACTIVE,
DEFERRED_CANCEL_ON_STOP,
DEFERRED_CANCEL_ON_DESTROY,
FINISHED,
}
/**
* Handles the runtime permission result map from the system dialog.<br><br>
* 시스템 다이얼로그의 런타임 권한 결과를 처리합니다.<br>
*
* @param results Map of permission to granted flag.<br><br>
* 권한과 허용 여부의 매핑입니다.<br>
*/
private fun handleRuntimePermissionsResult(results: Map<String, Boolean>) {
val completion = runtimeCompletion ?: return
val mappedResults = results.mapValues { (permission, granted) ->
val shouldShowRationale = runtimeHandler.shouldShowRationale(permission)
val wasRequestedBefore = runtimeRequestedBefore[permission]
?: runtimeHandler.wasRequested(permission)
runtimeHandler.mapResult(
permission = permission,
granted = granted,
shouldShowRationale = shouldShowRationale,
wasRequestedBefore = wasRequestedBefore,
)
}
resultAggregator.completeWaiters(mappedResults)
runtimeRequestedBefore = emptyMap()
completion.complete(Unit)
runtimeCompletion = null
}
/**
* Dispatches activity result callbacks to role and special handlers.<br><br>
* ActivityResult 콜백을 Role 및 특수 권한 처리기로 전달합니다.<br>
*
* Special permissions are completed immediately when ActivityResult arrives,
* while onResume remains as a fallback path for devices that return later.<br><br>
* 특수 권한은 ActivityResult가 도착하면 즉시 완료하고,
* onResume은 늦게 복귀하는 기기를 위한 fallback 경로로 유지합니다.<br>
*/
private fun handleActivityResultReturn() {
when (currentActivityAction) {
is InFlightActivityAction.Role -> handleRolePermissionReturn()
is InFlightActivityAction.Special -> handleSpecialPermissionReturn()
null -> Unit
}
}
/**
* Rechecks special permission state after returning from settings.<br><br>
* 설정 화면 복귀 후 특수 권한 상태를 재확인합니다.<br>
*/
private fun handleSpecialPermissionReturn() {
val action = currentActivityAction as? InFlightActivityAction.Special ?: return
if (!awaitingSpecialReturn) return
awaitingSpecialReturn = false
hasLeftForSpecial = false
currentActivityAction = null
val granted = specialHandler.isGranted(action.permission)
val result = if (granted) PermissionDecisionType.GRANTED else PermissionDecisionType.DENIED
resultAggregator.completeWaitersForPermissions(listOf(action.permission), result)
specialCompletion?.complete(Unit)
specialCompletion = null
}
/**
* Rechecks role state after returning from role request activity.<br><br>
* Role 요청 액티비티 복귀 후 Role 보유 상태를 재확인합니다.<br>
*/
private fun handleRolePermissionReturn() {
val action = currentActivityAction as? InFlightActivityAction.Role ?: return
currentActivityAction = null
val granted = roleHandler.isRoleHeld(action.role)
val result = if (granted) PermissionDecisionType.GRANTED else PermissionDecisionType.DENIED
resultAggregator.completeWaitersForPermissions(listOf(action.role), result)
roleCompletion?.complete(Unit)
roleCompletion = null
}
/**
* Returns whether the permission is supported on the current platform.<br><br>
* 현재 플랫폼에서 권한이 지원되는지 여부를 반환합니다.<br>
*
* @param permission Permission string to inspect.<br><br>
* 확인할 권한 문자열입니다.<br>
* @param type Classified permission type.<br><br>
* 분류된 권한 타입입니다.<br>
* @return Return value: true when the permission is supported. Log behavior: none.<br><br>
* 반환값: 지원되면 true입니다. 로그 동작: 없음.<br>
*/
private fun isPlatformPermissionAvailable(permission: String, type: PermissionType): Boolean = when (type) {
PermissionType.RUNTIME -> classifier.isSupported(permission)
PermissionType.SPECIAL -> classifier.isSupported(permission)
PermissionType.ROLE -> true
}
/**
* Suspends until the host reaches STARTED or returns false on destroy.<br><br>
* 호스트가 STARTED가 될 때까지 대기하거나 destroy 시 false를 반환합니다.<br>
*
* @return Return value: true when STARTED is reached, false when destroyed. Log behavior: none.<br><br>
* 반환값: STARTED 도달 시 true, destroy 시 false입니다. 로그 동작: 없음.<br>
*/
private suspend fun awaitHostStarted(): Boolean {
val lifecycle = host.lifecycleOwner.lifecycle
val currentState = lifecycle.currentState
if (currentState == Lifecycle.State.DESTROYED) return false
if (currentState.isAtLeast(Lifecycle.State.STARTED)) return true
return suspendCancellableCoroutine { continuation ->
val observer = object : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
lifecycle.removeObserver(this)
if (continuation.isActive) {
continuation.resume(true)
}
}
override fun onDestroy(owner: LifecycleOwner) {
lifecycle.removeObserver(this)
if (continuation.isActive) {
continuation.resume(false)
}
}
}
lifecycle.addObserver(observer)
continuation.invokeOnCancellation { lifecycle.removeObserver(observer) }
}
}
/**
* Represents the current activity action in flight.<br><br>
* 현재 실행 중인 액티비티 액션을 나타냅니다.<br>
*/
private sealed class InFlightActivityAction {
/**
* Action representing a special permission settings flow.<br><br>
* 특수 권한 설정 흐름을 나타내는 액션입니다.<br>
*
* @param permission Special permission string.<br><br>
* 특수 권한 문자열입니다.<br>
*/
data class Special(
val permission: String,
) : InFlightActivityAction()
/**
* Action representing a role request flow.<br><br>
* Role 요청 흐름을 나타내는 액션입니다.<br>
*
* @param role Role string requested via RoleManager.<br><br>
* RoleManager로 요청하는 Role 문자열입니다.<br>
*/
data class Role(
val role: String,
) : InFlightActivityAction()
}
}