Coverage Summary for Class: PermissionRequester (kr.open.library.simple_ui.xml.permissions.api)

Class Method, % Branch, % Line, % Instruction, %
PermissionRequester 59.3% (16/27) 27.1% (13/48) 39% (71/182) 49.8% (306/615)
PermissionRequester$Companion
PermissionRequester$ensureCoordinatorStartedWhenReady$observer$1 50% (2/4) 50% (2/4) 45.5% (5/11)
PermissionRequester$WhenMappings
Total 58.1% (18/31) 27.1% (13/48) 39.2% (73/186) 49.7% (311/626)


 package kr.open.library.simple_ui.xml.permissions.api
 
 import android.os.Bundle
 import androidx.activity.ComponentActivity
 import androidx.annotation.MainThread
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.DefaultLifecycleObserver
 import androidx.lifecycle.LifecycleOwner
 import androidx.lifecycle.lifecycleScope
 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.OrphanedDeniedRequestResult
 import kr.open.library.simple_ui.core.permissions.model.PermissionDecisionType
 import kr.open.library.simple_ui.core.permissions.model.PermissionDeniedItem
 import kr.open.library.simple_ui.core.permissions.model.PermissionDeniedType
 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.core.permissions.queue.PermissionQueue
 import kr.open.library.simple_ui.xml.permissions.coordinator.PermissionRequestCoordinator
 import kr.open.library.simple_ui.xml.permissions.coordinator.RequestEntry
 import kr.open.library.simple_ui.xml.permissions.flow.PermissionFlowProcessor
 import kr.open.library.simple_ui.xml.permissions.flow.RuntimePermissionHandler
 import kr.open.library.simple_ui.xml.permissions.host.PermissionHostAdapter
 import kr.open.library.simple_ui.xml.permissions.result.PermissionResultAggregator
 import kr.open.library.simple_ui.xml.permissions.state.PermissionStateStore
 import java.util.UUID
 
 /**
  * Coordinates runtime, special, and role permission requests through one API.<br><br>
  * 런타임/특수/Role 권한 요청을 하나의 API로 조정합니다.<br>
  *
  * The requester itself can be created before a Fragment is attached, but actual permission
  * execution requires a lifecycle-ready host.<br><br>
  * 요청기 자체는 Fragment attach 이전에도 생성할 수 있지만, 실제 권한 실행은 lifecycle 준비가 된
  * 호스트에서만 가능합니다.<br>
  *
  * @param host Host adapter supplying Activity/Fragment capabilities.<br><br>
  *             Activity/Fragment 기능을 제공하는 호스트 어댑터입니다.<br>
  */
 class PermissionRequester private constructor(
     private val host: PermissionHostAdapter,
 ) {
     /**
      * Creates a requester bound to an Activity host.<br>
      * Must be called on the **main thread**.<br><br>
      * Activity 호스트에 바인딩된 요청기를 생성합니다.<br>
      * **메인 스레드**에서만 호출해야 합니다.<br>
      *
      * @param activity Activity host used for permission requests.<br><br>
      *                 권한 요청에 사용되는 Activity 호스트입니다.<br>
      */
     @MainThread
     constructor(activity: ComponentActivity) : this(PermissionHostAdapter.ActivityHost(activity))
 
     /**
      * Creates a requester bound to a Fragment host.<br>
      * Must be called on the **main thread**.<br><br>
      * Fragment 호스트에 바인딩된 요청기를 생성합니다.<br>
      * **메인 스레드**에서만 호출해야 합니다.<br>
      *
      * @param fragment Fragment host used for permission requests.<br><br>
      *                 권한 요청에 사용되는 Fragment 호스트입니다.<br>
      */
     @MainThread
     constructor(fragment: Fragment) : this(PermissionHostAdapter.FragmentHost(fragment))
 
     /**
      * Companion container for internal constants.<br><br>
      * 내부 상수를 보관하는 companion 컨테이너입니다.<br>
      */
     companion object {
         /**
          * Empty permission placeholder used for empty request results.<br><br>
          * 빈 요청 결과에 사용하는 빈 권한 문자열입니다.<br>
          */
         private const val EMPTY_REQUEST_PERMISSION = ""
     }
 
     /**
      * State store used for Bundle snapshot persistence.<br><br>
      * Bundle 스냅샷 기반 상태 보존에 사용하는 스토어입니다.<br>
      */
     private val stateStore = PermissionStateStore()
 
     /**
      * Snapshot reference for persisted permission state.<br><br>
      * 보존된 권한 상태의 스냅샷 참조입니다.<br>
      */
     private val stateSnapshot = stateStore.getSnapshot()
 
     /**
      * Queue wrapper for request IDs.<br><br>
      * 요청 ID를 관리하는 큐 래퍼입니다.<br>
      */
     private val queue = PermissionQueue(stateSnapshot.requestQueue)
 
     /**
      * Permission classifier for runtime/special/role categorization.<br><br>
      * 런타임/특수/Role 분류를 위한 권한 분류기입니다.<br>
      */
     private val classifier: PermissionClassifier by lazy(LazyThreadSafetyMode.NONE) {
         PermissionClassifier(host.context)
     }
 
     /**
      * Runtime permission handler for rationale and result mapping.<br><br>
      * 런타임 권한의 설명 및 결과 매핑 처리기입니다.<br>
      */
     private val runtimeHandler: RuntimePermissionHandler = RuntimePermissionHandler(host, stateSnapshot.requestedHistory)
 
     /**
      * Special permission handler for settings intents and checks.<br><br>
      * 특수 권한의 설정 인텐트/상태 확인 처리기입니다.<br>
      */
     private val specialHandler: SpecialPermissionHandler by lazy(LazyThreadSafetyMode.NONE) {
         SpecialPermissionHandler(host.context)
     }
 
     /**
      * Role permission handler for RoleManager checks and intents.<br><br>
      * RoleManager 기반 역할 권한 처리기입니다.<br>
      */
     private val roleHandler: RolePermissionHandler by lazy(LazyThreadSafetyMode.NONE) {
         RolePermissionHandler(host.context)
     }
 
     /**
      * Active request entries keyed by request ID.<br><br>
      * requestId를 키로 하는 활성 요청 엔트리 맵입니다.<br>
      */
     private val requests: MutableMap<String, RequestEntry> = mutableMapOf()
 
     /**
      * Permission waiters for merging duplicate permission requests.<br><br>
      * 중복 권한 요청 병합을 위한 대기자 맵입니다.<br>
      */
     private val inFlightWaiters: MutableMap<String, MutableSet<String>> = mutableMapOf()
 
     /**
      * Aggregator for request results and persistence updates.<br><br>
      * 요청 결과와 상태 저장 업데이트를 집계하는 객체입니다.<br>
      */
     private val resultAggregator: PermissionResultAggregator by lazy(LazyThreadSafetyMode.NONE) {
         PermissionResultAggregator(
             stateStore = stateStore,
             stateSnapshot = stateSnapshot,
             queue = queue,
             requests = requests,
             inFlightWaiters = inFlightWaiters,
             classifier = classifier,
         )
     }
 
     /**
      * Processor that handles runtime/special/role permission flows.<br><br>
      * 런타임/특수/Role 권한 흐름을 처리하는 프로세서입니다.<br>
      */
     private val flowProcessor = PermissionFlowProcessor(
         host = host,
         classifierProvider = { classifier },
         runtimeHandler = runtimeHandler,
         specialHandlerProvider = { specialHandler },
         roleHandlerProvider = { roleHandler },
         resultAggregatorProvider = { resultAggregator },
     )
 
     /**
      * Coordinator that serializes request processing and restoration.<br><br>
      * 요청 처리/복원을 직렬화하는 조정자입니다.<br>
      */
     private val coordinator: PermissionRequestCoordinator by lazy(LazyThreadSafetyMode.NONE) {
         PermissionRequestCoordinator(
             queue = queue,
             stateSnapshot = stateSnapshot,
             requests = requests,
             inFlightWaiters = inFlightWaiters,
             scope = host.lifecycleOwner.lifecycleScope,
             flowProcessor = flowProcessor,
             resultAggregator = resultAggregator,
         )
     }
 
     /**
      * Flag indicating restoreState was already applied.<br><br>
      * restoreState가 이미 적용되었는지 나타내는 플래그입니다.<br>
      */
     private var hasRestoredState: Boolean = false
 
     /**
      * Flag indicating request processing has started.<br><br>
      * 요청 처리가 시작되었는지 나타내는 플래그입니다.<br>
      */
     private var hasRequestStarted: Boolean = false
 
     /**
      * Flag indicating the coordinator worker has started.<br><br>
      * 코디네이터 워커가 시작되었는지 나타내는 플래그입니다.<br>
      */
     private var isCoordinatorStarted: Boolean = false
 
     /**
      * Observer used when restored requests must wait for the host lifecycle to become ready.<br><br>
      * 복원된 요청이 host lifecycle 준비를 기다려야 할 때 사용하는 옵저버입니다.<br>
      */
     private var pendingCoordinatorStartObserver: DefaultLifecycleObserver? = null
 
     /**
      * Restores internal state from [savedInstanceState].<br>
      * Must be called on the **main thread**.<br><br>
      * [savedInstanceState]에서 내부 상태를 복원합니다.<br>
      * **메인 스레드**에서만 호출해야 합니다.<br>
      *
      * @param savedInstanceState Bundle containing saved state or null.<br><br>
      *                           저장된 상태를 담은 Bundle 또는 null입니다.<br>
      */
     @MainThread
     fun restoreState(savedInstanceState: Bundle?) {
         if (hasRequestStarted) {
             Logx.w("PermissionRequester: restoreState ignored because requests already started.")
             return
         }
         if (hasRestoredState) {
             Logx.w("PermissionRequester: restoreState ignored because it was already applied.")
             return
         }
         hasRestoredState = true
         stateStore.restoreState(savedInstanceState)
         ensureCoordinatorStartedWhenReady()
     }
 
     /**
      * Saves internal state into [outState].<br>
      * Must be called on the **main thread**.<br><br>
      * [outState]에 내부 상태를 저장합니다.<br>
      * **메인 스레드**에서만 호출해야 합니다.<br>
      *
      * @param outState Bundle that receives the saved state.<br><br>
      *                 저장 상태를 기록할 Bundle입니다.<br>
      */
     @MainThread
     fun saveState(outState: Bundle) {
         stateStore.saveState(outState)
     }
 
     /**
      * Requests a single permission and returns denied results via callback.<br>
      * Must be called on the **main thread**.<br><br>
      * 단일 권한을 요청하고 거부 결과를 콜백으로 반환합니다.<br>
      * **메인 스레드**에서만 호출해야 합니다.<br>
      *
      * @param permission Permission string to request.<br><br>
      *                  요청할 권한 문자열입니다.<br>
      * @param onDeniedResult Callback invoked with denied items.<br><br>
      *                       거부 항목을 전달받는 콜백입니다.<br>
      * @param onRationaleNeeded Callback for rationale UI when needed.<br><br>
      *                          Call `proceed()`, `cancel()`, or `defer(policy)` inside the callback. Returning without an action auto-cancels the flow.<br>
      *                          콜백 안에서 `proceed()`, `cancel()`, `defer(policy)` 중 하나를 호출해야 하며, 아무 액션 없이 반환되면 흐름은 자동 취소됩니다.<br>
      *                          필요 시 설명 UI를 제공하는 콜백입니다.<br>
      * @param onNavigateToSettings Callback for settings navigation when needed.<br><br>
      *                             Call `proceed()`, `cancel()`, or `defer(policy)` inside the callback. Returning without an action auto-cancels the flow.<br>
      *                             콜백 안에서 `proceed()`, `cancel()`, `defer(policy)` 중 하나를 호출해야 하며, 아무 액션 없이 반환되면 흐름은 자동 취소됩니다.<br>
      *                             필요 시 설정 이동을 안내하는 콜백입니다.<br>
      */
     @MainThread
     fun requestPermission(
         permission: String,
         onDeniedResult: (List<PermissionDeniedItem>) -> Unit,
         onRationaleNeeded: ((PermissionRationaleRequest) -> Unit)? = null,
         onNavigateToSettings: ((PermissionSettingsRequest) -> Unit)? = null,
     ) {
         requestPermissions(
             permissions = listOf(permission),
             onDeniedResult = onDeniedResult,
             onRationaleNeeded = onRationaleNeeded,
             onNavigateToSettings = onNavigateToSettings,
         )
     }
 
     /**
      * Requests multiple permissions and returns denied results via callback.<br>
      * The rationale/settings callbacks may switch to asynchronous UI by calling `defer(policy)`, and the default deferred policy is `CANCEL_ON_STOP`.<br>
      * rationale/settings 콜백은 `defer(policy)`를 호출해 비동기 UI로 전환할 수 있으며, 기본 defer 정책은 `CANCEL_ON_STOP`입니다.<br>
      * Must be called on the **main thread**.<br><br>
      * 여러 권한을 요청하고 거부 결과를 콜백으로 반환합니다.<br>
      * **메인 스레드**에서만 호출해야 합니다.<br>
      *
      * @param permissions Permissions to request.<br><br>
      *                    요청할 권한 목록입니다.<br>
      * @param onDeniedResult Callback invoked with denied items.<br><br>
      *                       거부 항목을 전달받는 콜백입니다.<br>
      * @param onRationaleNeeded Callback for rationale UI when needed.<br><br>
      *                          Call `proceed()`, `cancel()`, or `defer(policy)` inside the callback. Returning without an action auto-cancels the flow.<br>
      *                          콜백 안에서 `proceed()`, `cancel()`, `defer(policy)` 중 하나를 호출해야 하며, 아무 액션 없이 반환되면 흐름은 자동 취소됩니다.<br>
      *                          필요 시 설명 UI를 제공하는 콜백입니다.<br>
      * @param onNavigateToSettings Callback for settings navigation when needed.<br><br>
      *                             Call `proceed()`, `cancel()`, or `defer(policy)` inside the callback. Returning without an action auto-cancels the flow.<br>
      *                             콜백 안에서 `proceed()`, `cancel()`, `defer(policy)` 중 하나를 호출해야 하며, 아무 액션 없이 반환되면 흐름은 자동 취소됩니다.<br>
      *                             필요 시 설정 이동을 안내하는 콜백입니다.<br>
      */
     @MainThread
     fun requestPermissions(
         permissions: List<String>,
         onDeniedResult: (List<PermissionDeniedItem>) -> Unit,
         onRationaleNeeded: ((PermissionRationaleRequest) -> Unit)? = null,
         onNavigateToSettings: ((PermissionSettingsRequest) -> Unit)? = null,
     ) {
         if (handleEmptyRequest(permissions, onDeniedResult)) return
 
         val normalizedPermissions = normalizePermissions(permissions)
         if (handleLifecycleNotReady(normalizedPermissions, onDeniedResult)) return
 
         hasRequestStarted = true
         ensureCoordinatorStarted()
 
         val invalidPermissions = normalizedPermissions.filter { classifier.isInvalid(it) }.toSet()
         val requestId = UUID.randomUUID().toString()
         val results = mutableMapOf<String, PermissionDecisionType>()
         val pendingPermissions = mutableListOf<String>()
 
         for (permission in normalizedPermissions) {
             val decision = resolvePermission(requestId, permission)
             if (decision != null) {
                 results[permission] = decision
             } else {
                 pendingPermissions.add(permission)
             }
         }
 
         submitRequest(
             requestId, normalizedPermissions, results, pendingPermissions,
             onDeniedResult, onRationaleNeeded, onNavigateToSettings,
         )
     }
 
     /**
      * Handles empty permission request by invoking the denied callback.<br><br>
      * 빈 권한 요청을 처리하고 거부 콜백을 호출합니다.<br>
      *
      * @param permissions Original permission list to check.<br><br>
      *                    확인할 원본 권한 목록입니다.<br>
      * @param onDeniedResult Callback invoked with EMPTY_REQUEST denied item.<br><br>
      *                       EMPTY_REQUEST 거부 항목을 전달받는 콜백입니다.<br>
      * @return `true` if the request was empty and handled, `false` otherwise.<br><br>
      *         요청이 비어 있어 처리되었으면 `true`, 아니면 `false`.<br>
      */
     private fun handleEmptyRequest(permissions: List<String>, onDeniedResult: (List<PermissionDeniedItem>) -> Unit): Boolean {
         if (permissions.isEmpty()) {
             safeCatch {
                 onDeniedResult(listOf(PermissionDeniedItem(EMPTY_REQUEST_PERMISSION, PermissionDeniedType.EMPTY_REQUEST)))
             }
             return true
         }
         return false
     }
 
     /**
      * Handles lifecycle-not-ready state by invoking the denied callback.<br><br>
      * 라이프사이클 미준비 상태를 처리하고 거부 콜백을 호출합니다.<br>
      *
      * @param normalizedPermissions Deduplicated permission list.<br><br>
      *                              중복 제거된 권한 목록입니다.<br>
      * @param onDeniedResult Callback invoked with LIFECYCLE_NOT_READY denied items.<br><br>
      *                       LIFECYCLE_NOT_READY 거부 항목을 전달받는 콜백입니다.<br>
      * @return `true` if lifecycle was not ready and handled, `false` otherwise.<br><br>
      *         라이프사이클이 준비되지 않아 처리되었으면 `true`, 아니면 `false`.<br>
      */
     private fun handleLifecycleNotReady(
         normalizedPermissions: List<String>,
         onDeniedResult: (List<PermissionDeniedItem>) -> Unit,
     ): Boolean {
         if (!flowProcessor.isLifecycleRequestAllowed()) {
             safeCatch {
                 onDeniedResult(
                     normalizedPermissions.map { permission ->
                         PermissionDeniedItem(permission, PermissionDeniedType.LIFECYCLE_NOT_READY)
                     },
                 )
             }
             return true
         }
         return false
     }
 
     /**
      * Resolves a single permission's decision before actual request dispatch.<br><br>
      * 실제 요청 전에 단일 권한의 결정을 사전 해결합니다.<br>
      *
      * @param requestId Unique ID for the current request batch.<br><br>
      *                  현재 요청 배치의 고유 ID입니다.<br>
      * @param permission Permission string to resolve.<br><br>
      *                   해결할 권한 문자열입니다.<br>
      * @return Resolved decision type, or `null` if the permission needs runtime dispatch.<br><br>
      *         해결된 결정 타입, 또는 런타임 디스패치가 필요하면 `null`.<br>
      */
     private fun resolvePermission(requestId: String, permission: String): PermissionDecisionType? {
         if (classifier.isInvalid(permission)) {
             resultAggregator.logResult(
                 requestId = requestId,
                 permission = permission,
                 result = PermissionDecisionType.MANIFEST_UNDECLARED,
             )
             return PermissionDecisionType.MANIFEST_UNDECLARED
         }
         return when (classifier.classify(permission)) {
             PermissionType.ROLE -> resolveRolePermission(requestId, permission)
             PermissionType.SPECIAL -> resolveSpecialPermission(requestId, permission)
             PermissionType.RUNTIME -> resolveRuntimePermission(requestId, permission)
         }
     }
 
     /**
      * Resolves a role permission pre-check.<br><br>
      * Role 권한의 사전 확인을 수행합니다.<br>
      */
     private fun resolveRolePermission(requestId: String, permission: String): PermissionDecisionType? {
         if (!roleHandler.isRoleAvailable(permission)) {
             resultAggregator.logResult(
                 requestId = requestId,
                 permission = permission,
                 result = PermissionDecisionType.NOT_SUPPORTED,
             )
             return PermissionDecisionType.NOT_SUPPORTED
         }
         if (roleHandler.isRoleHeld(permission)) {
             resultAggregator.logResult(
                 requestId = requestId,
                 permission = permission,
                 result = PermissionDecisionType.GRANTED,
             )
             return PermissionDecisionType.GRANTED
         }
         return null
     }
 
     /**
      * Resolves a special permission pre-check.<br><br>
      * 특수 권한의 사전 확인을 수행합니다.<br>
      */
     private fun resolveSpecialPermission(requestId: String, permission: String): PermissionDecisionType? {
         if (!classifier.isSupported(permission)) {
             resultAggregator.logResult(
                 requestId = requestId,
                 permission = permission,
                 result = PermissionDecisionType.NOT_SUPPORTED,
             )
             return PermissionDecisionType.NOT_SUPPORTED
         }
         if (specialHandler.isGranted(permission)) {
             resultAggregator.logResult(
                 requestId = requestId,
                 permission = permission,
                 result = PermissionDecisionType.GRANTED,
             )
             return PermissionDecisionType.GRANTED
         }
         return null
     }
 
     /**
      * Resolves a runtime permission pre-check.<br><br>
      * 런타임 권한의 사전 확인을 수행합니다.<br>
      */
     private fun resolveRuntimePermission(requestId: String, permission: String): PermissionDecisionType? {
         if (!classifier.isSupported(permission)) {
             resultAggregator.logResult(
                 requestId = requestId,
                 permission = permission,
                 result = PermissionDecisionType.GRANTED,
             )
             return PermissionDecisionType.GRANTED
         }
 
         return when (classifier.getRuntimeRequestability(permission)) {
             RuntimePermissionRequestability.GRANTED_BY_DEFAULT -> {
                 resultAggregator.logResult(
                     requestId = requestId,
                     permission = permission,
                     result = PermissionDecisionType.GRANTED,
                 )
                 PermissionDecisionType.GRANTED
             }
 
             RuntimePermissionRequestability.NOT_SUPPORTED -> {
                 resultAggregator.logResult(
                     requestId = requestId,
                     permission = permission,
                     result = PermissionDecisionType.NOT_SUPPORTED,
                 )
                 PermissionDecisionType.NOT_SUPPORTED
             }
 
             RuntimePermissionRequestability.REQUESTABLE -> {
                 if (host.context.hasPermission(permission)) {
                     resultAggregator.logResult(
                         requestId = requestId,
                         permission = permission,
                         result = PermissionDecisionType.GRANTED,
                     )
                     PermissionDecisionType.GRANTED
                 } else {
                     null
                 }
             }
         }
     }
 
     /**
      * Registers the request entry and submits pending permissions to the coordinator.<br><br>
      * 요청 엔트리를 등록하고 미결 권한을 코디네이터에 제출합니다.<br>
      *
      * @param requestId Unique ID for the current request batch.<br><br>
      *                  현재 요청 배치의 고유 ID입니다.<br>
      * @param normalizedPermissions Deduplicated permission list.<br><br>
      *                              중복 제거된 권한 목록입니다.<br>
      * @param results Pre-resolved permission decisions.<br><br>
      *                사전 해결된 권한 결정 맵입니다.<br>
      * @param pendingPermissions Permissions requiring runtime dispatch.<br><br>
      *                           런타임 디스패치가 필요한 권한 목록입니다.<br>
      * @param onDeniedResult Callback invoked with denied items.<br><br>
      *                       거부 항목을 전달받는 콜백입니다.<br>
      * @param onRationaleNeeded Callback for rationale UI when needed.<br><br>
      *                          필요 시 설명 UI를 제공하는 콜백입니다.<br>
      * @param onNavigateToSettings Callback for settings navigation when needed.<br><br>
      *                             필요 시 설정 이동을 안내하는 콜백입니다.<br>
      */
     private fun submitRequest(
         requestId: String,
         normalizedPermissions: List<String>,
         results: MutableMap<String, PermissionDecisionType>,
         pendingPermissions: List<String>,
         onDeniedResult: (List<PermissionDeniedItem>) -> Unit,
         onRationaleNeeded: ((PermissionRationaleRequest) -> Unit)?,
         onNavigateToSettings: ((PermissionSettingsRequest) -> Unit)?,
     ) {
         val entry = RequestEntry(
             requestId = requestId,
             permissions = normalizedPermissions,
             results = results,
             isRestored = false,
             onDeniedResult = onDeniedResult,
             onRationaleNeeded = onRationaleNeeded,
             onNavigateToSettings = onNavigateToSettings,
         )
 
         resultAggregator.registerRequest(entry)
 
         if (pendingPermissions.isNotEmpty()) {
             resultAggregator.registerWaiters(requestId, pendingPermissions)
             coordinator.enqueueRequest(requestId)
         }
 
         resultAggregator.tryCompleteRequest(requestId)
     }
 
     /**
      * Returns and clears orphaned denied results after process restore.<br><br>
      * 프로세스 복원 후 orphaned 거부 결과를 반환하고 비웁니다.<br>
      *
      * @return Return value: list of orphaned denied request results. Log behavior: none.<br><br>
      *         반환값: orphaned 거부 요청 결과 목록입니다. 로그 동작: 없음.<br>
      */
     fun consumeOrphanedDeniedResults(): List<OrphanedDeniedRequestResult> =
         resultAggregator.consumeOrphanedDeniedResults()
 
     /**
      * Ensures the coordinator worker is started once.<br><br>
      * 코디네이터 워커가 한 번만 시작되도록 보장합니다.<br>
      */
     private fun ensureCoordinatorStarted() {
         if (isCoordinatorStarted) return
         clearPendingCoordinatorStartObserver()
         coordinator.start()
         isCoordinatorStarted = true
     }
 
     /**
      * Starts the coordinator immediately when lifecycle is ready, or waits until create/start.<br><br>
      * lifecycle이 준비되면 즉시 코디네이터를 시작하고, 아니면 create/start 시점까지 대기합니다.<br>
      */
     private fun ensureCoordinatorStartedWhenReady() {
         if (isCoordinatorStarted) return
         if (flowProcessor.isLifecycleRequestAllowed()) {
             ensureCoordinatorStarted()
             return
         }
         if (pendingCoordinatorStartObserver != null) return
 
         val lifecycle = host.lifecycleOwner.lifecycle
         val observer = object : DefaultLifecycleObserver {
             override fun onCreate(owner: LifecycleOwner) {
                 tryStartCoordinatorOnLifecycleReady()
             }
 
             override fun onStart(owner: LifecycleOwner) {
                 tryStartCoordinatorOnLifecycleReady()
             }
 
             override fun onDestroy(owner: LifecycleOwner) {
                 clearPendingCoordinatorStartObserver()
             }
         }
 
         pendingCoordinatorStartObserver = observer
         lifecycle.addObserver(observer)
     }
 
     /**
      * Attempts to start the coordinator when the lifecycle satisfies request preconditions.<br><br>
      * lifecycle이 요청 전제 조건을 만족할 때 코디네이터 시작을 시도합니다.<br>
      */
     private fun tryStartCoordinatorOnLifecycleReady() {
         if (isCoordinatorStarted) {
             clearPendingCoordinatorStartObserver()
             return
         }
         if (!flowProcessor.isLifecycleRequestAllowed()) return
         ensureCoordinatorStarted()
     }
 
     /**
      * Clears the pending lifecycle observer used for deferred coordinator start.<br><br>
      * 지연 코디네이터 시작에 사용하는 대기 중 lifecycle 옵저버를 정리합니다.<br>
      */
     private fun clearPendingCoordinatorStartObserver() {
         val observer = pendingCoordinatorStartObserver ?: return
         host.lifecycleOwner.lifecycle.removeObserver(observer)
         pendingCoordinatorStartObserver = null
     }
 
     /**
      * Normalizes permission list by removing duplicates while keeping order.<br><br>
      * 권한 목록의 중복을 제거하면서 순서를 유지합니다.<br>
      *
      * @param permissions Permissions to normalize.<br><br>
      *                    정규화할 권한 목록입니다.<br>
      * @return Return value: normalized permission list. Log behavior: none.<br><br>
      *         반환값: 정규화된 권한 목록입니다. 로그 동작: 없음.<br>
      */
     private fun normalizePermissions(permissions: List<String>): List<String> = permissions.distinct()
 }