Coverage Summary for Class: PermissionClassifier (kr.open.library.simple_ui.core.permissions.classifier)

Class Method, % Branch, % Line, % Instruction, %
PermissionClassifier 90% (9/10) 84.2% (32/38) 85% (34/40) 88.6% (163/184)
PermissionClassifier$Companion
Total 90% (9/10) 84.2% (32/38) 85% (34/40) 88.6% (163/184)


 package kr.open.library.simple_ui.core.permissions.classifier
 
 import android.Manifest
 import android.content.Context
 import android.content.pm.PermissionInfo
 import android.os.Build
 import kr.open.library.simple_ui.core.logcat.Logx
 import kr.open.library.simple_ui.core.permissions.extensions.getPermissionBaseProtectionLevel
 import kr.open.library.simple_ui.core.permissions.internal.readDeclaredManifestPermissions
 import kr.open.library.simple_ui.core.permissions.vo.PermissionConstants
 import kr.open.library.simple_ui.core.permissions.vo.PermissionSpecialType
 
 /**
  * 요청 흐름에서 사용하는 권한 분류 유형입니다.<br><br>
  * Defines permission categories used by the requester.<br>
  */
 enum class PermissionType {
     /**
      * `ROLE`, `SPECIAL`을 제외한 일반 권한 처리 분류입니다.
      * 이 분류에 속한 권한은 이후 단계에서 `dangerous`, `normal`, `signature` 계열로 다시 세분화될 수 있습니다.<br><br>
      * General permission flow category after excluding role and special types.
      * Entries in this group may later resolve to requestable dangerous permissions,
      * granted-by-default normal permissions, or not-supported signature-style permissions.<br>
      */
     RUNTIME,
 
     /**
      * 설정 화면 이동이 필요한 특수 권한 분류입니다.<br><br>
      * Special permissions that require settings navigation.<br>
      */
     SPECIAL,
 
     /**
      * `RoleManager`를 통해 처리되는 역할 요청 분류입니다.<br><br>
      * Role requests managed by RoleManager.<br>
      */
     ROLE,
 }
 
 /**
  * 권한이 표준 런타임 다이얼로그 흐름에 진입할 수 있는지를 나타냅니다.<br><br>
  * Represents whether a permission can enter the standard runtime dialog flow.<br>
  */
 enum class RuntimePermissionRequestability {
     /**
      * 런타임 다이얼로그로 요청 가능한 `dangerous` 권한입니다.<br><br>
      * Dangerous permission that can be requested through the runtime dialog.<br>
      */
     REQUESTABLE,
 
     /**
      * 별도 요청 없이 기본 허용으로 간주해야 하는 `normal` 권한입니다.<br><br>
      * Normal permission that should be treated as granted by default.<br>
      */
     GRANTED_BY_DEFAULT,
 
     /**
      * 일반 앱 프로세스 기준으로 런타임 요청 대상으로 취급하지 않는 권한입니다.<br><br>
      * Permission that ordinary app processes should not request through runtime flow.<br>
      */
     NOT_SUPPORTED,
 }
 
 /**
  * 권한을 분류하고, SDK 지원 여부, 매니페스트 선언 여부, 런타임 요청 가능 여부를 판단합니다.<br><br>
  * Classifies permissions and validates support, manifest declarations, and runtime requestability.<br>
  *
  * @param context 패키지 권한 메타데이터를 읽기 위한 컨텍스트입니다.<br><br>
  *                Context used to read package permission metadata.<br>
  */
 class PermissionClassifier(
     private val context: Context,
 ) {
     /**
      * Role 접두사와 특수 앱 접근 권한 목록을 담은 정적 상수입니다.<br><br>
      * Static constants for role prefix and special app access permissions.<br>
      */
     companion object {
         /**
          * Role 문자열을 식별하는 접두사입니다.<br><br>
          * Prefix that identifies Role strings.<br>
          */
         private const val ROLE_PREFIX = "android.app.role."
 
         /**
          * 특수 앱 접근 항목으로 취급하는 권한 목록입니다.<br><br>
          * Permissions treated as special app access entries.<br>
          */
         private val SPECIAL_APP_ACCESS = setOf(
             Manifest.permission.BIND_ACCESSIBILITY_SERVICE,
             Manifest.permission.BIND_NOTIFICATION_LISTENER_SERVICE,
         )
     }
 
     /**
      * 매니페스트에 선언된 권한 집합의 캐시입니다.<br><br>
      * Cached set of permissions declared in the manifest.<br>
      */
     private val declaredPermissions: Set<String> by lazy { loadDeclaredPermissions() }
 
     /**
      * 권한을 `RUNTIME`, `SPECIAL`, `ROLE` 중 하나로 분류합니다.<br><br>
      * Classifies the permission into runtime, special, or role.<br>
      *
      * @param permission 분류할 권한 문자열입니다.<br><br>
      *                  Permission string to classify.<br>
      * @return 분류된 권한 유형입니다. 이 메서드는 로그를 남기지 않습니다.<br><br>
      *         Return value: classified permission type. Log behavior: none.<br>
      */
     fun classify(permission: String): PermissionType = when {
         isRole(permission) -> PermissionType.ROLE
         isSpecial(permission) -> PermissionType.SPECIAL
         else -> PermissionType.RUNTIME
     }
 
     /**
      * 권한 문자열이 `Role` 형식인지 여부를 반환합니다.<br><br>
      * Returns whether the permission is a Role string.<br>
      *
      * @param permission 확인할 권한 문자열입니다.<br><br>
      *                  Permission string to inspect.<br>
      * @return Role 문자열이면 `true`를 반환합니다. 이 메서드는 로그를 남기지 않습니다.<br><br>
      *         Return value: true when the string is a Role. Log behavior: none.<br>
      */
     fun isRole(permission: String): Boolean = permission.startsWith(ROLE_PREFIX)
 
     /**
      * 권한을 특수 권한으로 취급해야 하는지 반환합니다.<br><br>
      * Returns whether the permission is treated as special.<br>
      *
      * `MANAGE_MEDIA`는 일반 런타임 흐름에서 제외하기 위해 `SPECIAL`로 분류되지만,
      * 이 라이브러리에서는 일반 앱 요청 대상으로는 계속 지원하지 않는 권한으로 취급합니다.<br><br>
      * `MANAGE_MEDIA` is classified as `SPECIAL` so it is excluded from the general runtime flow,
      * but this library still treats it as unsupported for ordinary app requests.<br>
      *
      * @param permission 확인할 권한 문자열입니다.<br><br>
      *                  Permission string to inspect.<br>
      * @return 특수 권한이면 `true`를 반환합니다. 이 메서드는 로그를 남기지 않습니다.<br><br>
      *         Return value: true when special. Log behavior: none.<br>
      */
     fun isSpecial(permission: String): Boolean =
         permission == Manifest.permission.MANAGE_MEDIA ||
             SPECIAL_APP_ACCESS.contains(permission) ||
             PermissionSpecialType.entries.any { it.permission == permission }
 
     /**
      * 권한이 특수 앱 접근 항목인지 여부를 반환합니다.<br><br>
      * Returns whether the permission is a special app access entry.<br>
      *
      * @param permission 확인할 권한 문자열입니다.<br><br>
      *                  Permission string to inspect.<br>
      * @return 특수 앱 접근 항목이면 `true`를 반환합니다. 이 메서드는 로그를 남기지 않습니다.<br><br>
      *         Return value: true when it is special app access. Log behavior: none.<br>
      */
     fun isSpecialAppAccess(permission: String): Boolean = SPECIAL_APP_ACCESS.contains(permission)
 
     /**
      * 현재 SDK에서 이 권한을 지원하는지 여부를 반환합니다.<br><br>
      * Returns whether the permission is supported on this SDK level.<br>
      *
      * `MANAGE_MEDIA`는 OS 버전상 존재하더라도 일반 앱 요청 대상으로는 노출하지 않는
      * 라이브러리 정책 때문에 의도적으로 `false`를 반환합니다.<br><br>
      * `MANAGE_MEDIA` is intentionally returned as unsupported even on supported OS versions,
      * because the library policy does not expose it as a requestable permission for ordinary apps.<br>
      *
      * @param permission 확인할 권한 문자열입니다.<br><br>
      *                  Permission string to inspect.<br>
      * @return 현재 SDK에서 지원하면 `true`를 반환합니다. 이 메서드는 로그를 남기지 않습니다.<br><br>
      *         Return value: true when supported. Log behavior: none.<br>
      *
      * **Note / 주의:** The `else → true` branch means any permission not listed in
      * [PermissionConstants.ApiLevelRequirements] is treated as universally supported.
      * When a new Android version introduces new API-level-gated permissions, add them to the
      * corresponding set in [PermissionConstants.ApiLevelRequirements] and add a matching branch here.<br><br>
      * `else → true` 분기로 인해 [PermissionConstants.ApiLevelRequirements]에 없는 권한은
      * 모든 API 레벨에서 지원되는 것으로 처리됩니다.
      * 신규 Android 버전에서 API 레벨 제한 권한이 추가되면 해당 집합과 분기를 반드시 추가하세요.<br>
      */
     fun isSupported(permission: String): Boolean = when {
         permission == Manifest.permission.MANAGE_MEDIA -> false
         PermissionConstants.ApiLevelRequirements.ANDROID_R_PERMISSIONS.contains(permission) ->
             Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
         PermissionConstants.ApiLevelRequirements.ANDROID_S_PERMISSIONS.contains(permission) ->
             Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
         PermissionConstants.ApiLevelRequirements.ANDROID_TIRAMISU_PERMISSIONS.contains(permission) ->
             Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
         PermissionConstants.ApiLevelRequirements.ANDROID_U_PERMISSIONS.contains(permission) ->
             Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
         else -> true
     }
 
     /**
      * 권한이 런타임 다이얼로그 흐름에 진입 가능한지 반환합니다.<br><br>
      * Returns whether the permission can enter the runtime dialog flow.<br>
      *
      * 이 메서드는 호출부가 [isInvalid]를 통해 빈 문자열이나 매니페스트 미선언 권한 같은
      * 잘못된 입력을 먼저 걸렀다고 가정합니다.
      * 즉, 이미 유효성 검증이 끝난 일반 권한에 대해서만 요청 가능성을 판정합니다.<br><br>
      * This method assumes the caller already filtered out invalid inputs such as blank strings
      * or manifest-undeclared permissions via [isInvalid]. It only classifies requestability for
      * already-validated general permissions.<br>
      *
      * @param permission 확인할 권한 문자열입니다.<br><br>
      *                  Permission string to inspect.<br>
      * @return 런타임 요청 가능성 분류입니다. 이 메서드는 로그를 남기지 않습니다.<br><br>
      *         Return value: runtime requestability classification. Log behavior: none.<br>
      */
     fun getRuntimeRequestability(permission: String): RuntimePermissionRequestability {
         if (classify(permission) != PermissionType.RUNTIME) {
             return RuntimePermissionRequestability.NOT_SUPPORTED
         }
 
         return when (context.getPermissionBaseProtectionLevel(permission)) {
             PermissionInfo.PROTECTION_DANGEROUS -> RuntimePermissionRequestability.REQUESTABLE
             PermissionInfo.PROTECTION_NORMAL -> RuntimePermissionRequestability.GRANTED_BY_DEFAULT
             else -> RuntimePermissionRequestability.NOT_SUPPORTED
         }
     }
 
     /**
      * 권한을 잘못된 입력으로 취급해야 하는지 반환합니다.<br><br>
      * Returns whether the permission should be treated as invalid.<br>
      *
      * @param permission 검증할 권한 문자열입니다.<br><br>
      *                  Permission string to validate.<br>
      * @return 잘못된 입력이면 `true`를 반환합니다. 이 메서드는 로그를 남기지 않습니다.<br><br>
      *         Return value: true when invalid. Log behavior: none.<br>
      */
     fun isInvalid(permission: String): Boolean {
         if (permission.isEmpty()) return true
         if (isRole(permission)) return false
         if (isSpecialAppAccess(permission)) return false
         return !declaredPermissions.contains(permission)
     }
 
     /**
      * 앱 매니페스트에 선언된 권한 목록을 읽어 옵니다.<br><br>
      * Loads permissions declared in the app manifest.<br>
      *
      * @return 선언된 권한 집합입니다. 권한 목록이 비어 있으면 경고 로그를 남깁니다.<br><br>
      *         Return value: declared permissions set. Log behavior: logs a warning when empty.<br>
      */
     private fun loadDeclaredPermissions(): Set<String> = context.readDeclaredManifestPermissions().also { permissions ->
         if (permissions.isEmpty()) {
             Logx.w("PermissionClassifier: requestedPermissions is empty.")
         }
     }
 }