Coverage Summary for Class: RootDialogFragment (kr.open.library.simple_ui.xml.ui.components.dialog.root)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| RootDialogFragment |
100%
(1/1)
|
38.9%
(7/18)
|
6.5%
(3/46)
|
33.3%
(16/48)
|
28.7%
(98/342)
|
package kr.open.library.simple_ui.xml.ui.components.dialog.root
import android.app.Dialog
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.Gravity
import android.view.View
import androidx.annotation.CallSuper
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StyleRes
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
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.model.OrphanedDeniedRequestResult
import kr.open.library.simple_ui.core.permissions.model.PermissionDeniedItem
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.api.PermissionRequester
import kr.open.library.simple_ui.xml.permissions.register.PermissionRequestInterface
/**
* Root DialogFragment class providing common dialog functionality and permission management.<br>
* Serves as the foundation for all DialogFragment classes in the library.<br><br>
* 공통 다이얼로그 기능과 권한 관리를 제공하는 루트 DialogFragment 클래스입니다.<br>
* 라이브러리의 모든 DialogFragment 클래스의 기반이 됩니다.<br>
*
* **Why this class exists / 이 클래스가 필요한 이유:**<br>
* - Android's DialogFragment requires repetitive setup for common dialog features like positioning, animation, and background customization.<br>
* - Permission management needs careful lifecycle handling to survive configuration changes.<br>
* - Dialog show/dismiss operations can throw exceptions in edge cases (fragment detached, state loss), requiring safe wrappers.<br>
* - Provides a centralized foundation for all dialog variants (normal, ViewBinding, DataBinding) to inherit common functionality.<br><br>
* - Android의 DialogFragment는 위치, 애니메이션, 배경 커스터마이징과 같은 공통 다이얼로그 기능에 대해 반복적인 설정이 필요합니다.<br>
* - 권한 관리는 구성 변경에서 살아남기 위해 신중한 생명주기 처리가 필요합니다.<br>
* - 다이얼로그 show/dismiss 작업은 엣지 케이스(프래그먼트 분리, 상태 손실)에서 예외를 던질 수 있어 안전한 래퍼가 필요합니다.<br>
* - 모든 다이얼로그 변형(normal, ViewBinding, DataBinding)이 공통 기능을 상속받을 수 있는 중앙화된 기반을 제공합니다.<br>
*
* **Design decisions / 설계 결정 이유:**<br>
* - Extends DialogFragment to provide dialog-specific lifecycle and features.<br>
* - Uses PermissionRequester pattern for reusable permission handling across DialogFragment/Fragment/Activity.<br>
* - Provides abstract setBackgroundColor/setBackgroundResource methods for subclasses to implement based on their view access pattern.<br>
* - Uses safeCatch wrapper for show/dismiss operations to prevent crashes from state loss exceptions.<br>
* - Stores dialog configuration (gravity, animation, cancelable) as properties to allow dynamic updates after creation.<br><br>
* - DialogFragment를 확장하여 다이얼로그 전용 생명주기 및 기능을 제공합니다.<br>
* - DialogFragment/Fragment/Activity에서 재사용 가능한 권한 처리를 위해 PermissionRequester 패턴을 사용합니다.<br>
* - 하위 클래스가 뷰 접근 패턴에 따라 구현할 수 있도록 추상 setBackgroundColor/setBackgroundResource 메서드를 제공합니다.<br>
* - 상태 손실 예외로 인한 크래시를 방지하기 위해 show/dismiss 작업에 safeCatch 래퍼를 사용합니다.<br>
* - 생성 후 동적 업데이트를 허용하기 위해 다이얼로그 구성(gravity, animation, cancelable)을 프로퍼티로 저장합니다.<br>
*
* **Important notes / 주의사항:**<br>
* - Permission requests must be made only after the DialogFragment is attached (isAdded == true).<br>
* - Calling onRequestPermissions() before attachment is not supported; call only after the DialogFragment is attached.<br>
* - Use safeShow/safeDismiss instead of show/dismiss to prevent crashes from IllegalStateException.<br><br>
* - 권한 요청은 DialogFragment가 attach된 이후(isAdded == true)에만 수행해야 합니다.<br>
* - attach 이전의 onRequestPermissions() 호출은 지원하지 않으며, DialogFragment가 attach된 뒤에만 호출해야 합니다.<br>
* - IllegalStateException으로 인한 크래시를 방지하기 위해 show/dismiss 대신 safeShow/safeDismiss를 사용하세요.<br>
*
* **Features / 기능:**<br>
* - Custom animation styles for dialog appearance/disappearance<br>
* - Dialog position control via gravity settings<br>
* - Cancelable behavior configuration<br>
* - Background color and drawable customization<br>
* - Click listener support (positive, negative, other)<br>
* - Safe show/dismiss methods with exception handling<br>
* - Runtime permission management via PermissionRequester<br>
* - Dialog size resizing based on screen ratio<br><br>
* - 다이얼로그 나타남/사라짐에 대한 커스텀 애니메이션 스타일<br>
* - gravity 설정을 통한 다이얼로그 위치 제어<br>
* - 취소 가능 동작 구성<br>
* - 배경색 및 drawable 커스터마이징<br>
* - 클릭 리스너 지원 (positive, negative, other)<br>
* - 예외 처리가 포함된 안전한 show/dismiss 메서드<br>
* - PermissionRequester를 통한 런타임 권한 관리<br>
* - 화면 비율 기반 다이얼로그 크기 조정<br>
*
* @see BaseDialogFragment For simple layout-based DialogFragment.<br><br>
* 간단한 레이아웃 기반 DialogFragment는 BaseDialogFragment를 참조하세요.<br>
*
* @see ParentBindingViewDialogFragment For the abstract parent class of all binding-enabled dialog fragments.<br><br>
* 모든 바인딩 지원 DialogFragment의 추상 부모 클래스는 ParentBindingViewDialogFragment를 참조하세요.<br>
*
* @see BaseViewBindingDialogFragment For ViewBinding-enabled DialogFragment.<br><br>
* ViewBinding을 사용하는 DialogFragment는 BaseViewBindingDialogFragment를 참조하세요.<br>
*
* @see BaseDataBindingDialogFragment For DataBinding-enabled DialogFragment.<br><br>
* DataBinding을 사용하는 DialogFragment는 BaseDataBindingDialogFragment를 참조하세요.<br>
*/
public abstract class RootDialogFragment :
DialogFragment(),
PermissionRequestInterface {
protected val config = DialogConfig()
/**
* Delegate for handling runtime permission requests.<br><br>
* 런타임 권한 요청을 처리하는 델리게이트입니다.<br>
*/
private lateinit var permissionRequester: PermissionRequester
/**
* Sets the background color of the dialog.<br>
* Clears any previously set background drawable.<br>
* If rootView is null, only stores the color to be applied when view is created.<br><br>
* 다이얼로그의 배경색을 설정합니다.<br>
* 이전에 설정된 배경 drawable을 지웁니다.<br>
* rootView가 null인 경우, 뷰 생성 시 적용될 색상만 저장합니다.<br>
*
* @param rootView The root view to apply the background color to, or null to only store the color.<br><br>
* 배경색을 적용할 루트 뷰, 또는 색상만 저장하려면 null.<br>
* @param color The color value to set as background.<br><br>
* 배경으로 설정할 색상 값.<br>
*/
protected fun setBackgroundColor(@ColorInt color: Int, rootView: View) {
config.setBackgroundColor(color, rootView)
}
public fun setBackgroundColor(@ColorInt color: Int) {
config.setBackgroundColor(color)
}
/**
* Sets the background drawable of the dialog.<br>
* Clears any previously set background color.<br>
* If rootView is null, only stores the resource ID to be applied when view is created.<br><br>
* 다이얼로그의 배경 drawable을 설정합니다.<br>
* 이전에 설정된 배경색을 지웁니다.<br>
* rootView가 null인 경우, 뷰 생성 시 적용될 리소스 ID만 저장합니다.<br>
*
* @param rootView The root view to apply the background resource to, or null to only store the resource ID.<br><br>
* 배경 리소스를 적용할 루트 뷰, 또는 리소스 ID만 저장하려면 null.<br>
* @param resId The drawable resource ID to set as background.<br><br>
* 배경으로 설정할 drawable 리소스 ID.<br>
*/
protected fun setBackgroundResource(@DrawableRes resId: Int, rootView: View? = null) {
config.setBackgroundResource(resId, rootView)
}
public fun setBackgroundResource(@DrawableRes resId: Int) {
config.setBackgroundResource(resId)
}
@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
permissionRequester = PermissionRequester(this)
permissionRequester.restoreState(savedInstanceState)
}
@CallSuper
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
// DialogFragment.setCancelable()을 먼저 호출하여 mCancelable 필드를 갱신합니다.
// super.onCreateDialog() 내부에서 mCancelable 기준으로 Dialog를 설정하므로,
// Dialog.setCancelable()을 직접 호출하면 super 호출 후 덮어써져 무효화됩니다.
setCancelable(config.dialogCancelable)
return super.onCreateDialog(savedInstanceState).apply {
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
// Apply animation if set
config.animationStyle?.let { style ->
window?.attributes?.windowAnimations = style
}
// Apply gravity if not center
if (config.dialogGravity != Gravity.CENTER) {
window?.setGravity(config.dialogGravity)
}
}
}
@CallSuper
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (::permissionRequester.isInitialized) {
permissionRequester.saveState(outState)
}
}
/**
* Resizes the dialog based on screen size ratios.<br>
* Width is calculated as screen width * widthRatio, or WRAP_CONTENT if null.<br>
* Height is calculated as screen height * heightRatio, or WRAP_CONTENT if null.<br><br>
* 화면 크기 비율을 기반으로 다이얼로그 크기를 조정합니다.<br>
* 너비는 화면 너비 * widthRatio로 계산되며, null이면 WRAP_CONTENT입니다.<br>
* 높이는 화면 높이 * heightRatio로 계산되며, null이면 WRAP_CONTENT입니다.<br>
*
* @param widthRatio The ratio of screen width (0.0 to 1.0), or null for WRAP_CONTENT.
* Any non-null value outside 0.0..1.0 throws an exception.<br><br>
* 화면 너비 비율 (0.0 ~ 1.0), null이면 WRAP_CONTENT입니다.
* null이 아닌 값이 0.0 ~ 1.0 범위를 벗어나면 예외가 발생합니다.<br>
*
* @param heightRatio The ratio of screen height (0.0 to 1.0), or null for WRAP_CONTENT.
* Any non-null value outside 0.0..1.0 throws an exception.<br><br>
* 화면 높이 비율 (0.0 ~ 1.0), null이면 WRAP_CONTENT입니다.
* null이 아닌 값이 0.0 ~ 1.0 범위를 벗어나면 예외가 발생합니다.<br>
*/
protected fun resizeDialog(widthRatio: Float?, heightRatio: Float?) {
dialog?.window?.let { window ->
safeCatch {
config.resizeDialog(window, requireActivity(), widthRatio, heightRatio)
}
} ?: Logx.e("Error dialog window is null!")
}
/**
* Sets the custom animation style for dialog appearance/disappearance.<br>
* Applied immediately if dialog is already showing.<br><br>
* 다이얼로그 나타남/사라짐에 대한 커스텀 애니메이션 스타일을 설정합니다.<br>
* 다이얼로그가 이미 표시 중인 경우 즉시 적용됩니다.<br>
*
* @param style The animation style resource ID.<br><br>
* 애니메이션 스타일 리소스 ID.<br>
*/
public fun setAnimationStyle(@StyleRes style: Int) {
config.animationStyle = style
dialog?.window?.attributes?.windowAnimations = style
}
/**
* Sets the position of the dialog on screen.<br>
* Applied immediately if dialog is already showing.<br><br>
* 화면에서 다이얼로그의 위치를 설정합니다.<br>
* 다이얼로그가 이미 표시 중인 경우 즉시 적용됩니다.<br>
*
* @param gravity The gravity value for positioning (e.g., Gravity.BOTTOM, Gravity.TOP).<br><br>
* 위치 지정을 위한 gravity 값 (예: Gravity.BOTTOM, Gravity.TOP).<br>
*/
public fun setDialogGravity(gravity: Int) {
config.dialogGravity = gravity
dialog?.window?.setGravity(gravity)
}
/**
* Sets whether the dialog can be canceled by pressing back button or touching outside.<br>
* Applied immediately if dialog is already showing.<br><br>
* 뒤로 가기 버튼을 누르거나 외부를 터치하여 다이얼로그를 취소할 수 있는지 설정합니다.<br>
* 다이얼로그가 이미 표시 중인 경우 즉시 적용됩니다.<br>
*
* @param cancelable True to allow cancellation, false to prevent it.<br><br>
* 취소를 허용하려면 true, 방지하려면 false.<br>
*/
public fun setCancelableDialog(cancelable: Boolean) {
config.dialogCancelable = cancelable
// DialogFragment.setCancelable()을 사용하여 mCancelable 필드와 Dialog를 동시에 갱신합니다.
// dialog?.setCancelable()은 Dialog 객체만 갱신하여 구성 변경 시 상태가 유실됩니다.
setCancelable(cancelable)
}
/**
* Safely dismisses the dialog with exception handling.<br>
* Catches any exceptions that may occur during dismissal.<br><br>
* 예외 처리와 함께 다이얼로그를 안전하게 닫습니다.<br>
* 닫는 동안 발생할 수 있는 모든 예외를 잡습니다.<br>
*/
public fun safeDismiss() = safeCatch {
dismiss()
}
/**
* Safely shows the dialog with exception handling.<br>
* Catches any exceptions that may occur during showing.<br><br>
* 예외 처리와 함께 다이얼로그를 안전하게 표시합니다.<br>
* 표시하는 동안 발생할 수 있는 모든 예외를 잡습니다.<br>
*
* @param fragmentManager The FragmentManager to use for showing the dialog.<br><br>
* 다이얼로그를 표시하는 데 사용할 FragmentManager.<br>
*
* @param tag The tag for this fragment, as per FragmentTransaction.add.<br><br>
* FragmentTransaction.add에 따른 이 프래그먼트의 태그.<br>
*/
public fun safeShow(fragmentManager: FragmentManager, tag: String) = safeCatch {
show(fragmentManager, tag)
}
/**
* Requests multiple permissions and returns denied results via callback.<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>
* 필요 시 rationale 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>
*/
@CallSuper
final override fun requestPermissions(
permissions: List<String>,
onDeniedResult: (List<PermissionDeniedItem>) -> Unit,
onRationaleNeeded: ((PermissionRationaleRequest) -> Unit)?,
onNavigateToSettings: ((PermissionSettingsRequest) -> Unit)?
) {
check(::permissionRequester.isInitialized) { "permissionRequester is not initialized. Please call super.onCreate() first." }
check(isAdded) { "Permission request must be called after Fragment is attached (isAdded == true)." }
permissionRequester.requestPermissions(permissions, onDeniedResult, onRationaleNeeded, onNavigateToSettings)
}
/**
* Requests permissions using the delegate.<br>
* Call only after the Fragment is attached (isAdded == true).<br><br>
* 델리게이트를 사용하여 권한을 요청합니다.<br>
* Fragment가 attach된 이후(isAdded == true)에만 호출하세요.<br>
*
* @param permissions Permissions to request.<br><br>
* 요청할 권한 목록입니다.<br>
* @param onDeniedResult Callback invoked with denied items.<br><br>
* 거부 항목을 전달받는 콜백입니다.<br>
*/
@CallSuper
final override fun requestPermissions(permissions: List<String>, onDeniedResult: (List<PermissionDeniedItem>) -> Unit) {
check(::permissionRequester.isInitialized) { "permissionRequester is not initialized. Please call super.onCreate() first." }
check(isAdded) { "Permission request must be called after Fragment is attached (isAdded == true)." }
permissionRequester.requestPermissions(permissions, onDeniedResult, null, null)
}
/**
* Returns and clears denied results that lost their callbacks after process restore.<br>
* Call this in [onCreate] to handle results from requests that were interrupted by process kill.<br><br>
* 프로세스 복원 후 콜백을 잃은 거부 결과를 반환하고 비웁니다.<br>
* 프로세스 킬로 중단된 요청의 결과를 처리하려면 [onCreate]에서 호출하세요.<br>
*
* @return Return value: list of orphaned denied request results. Log behavior: none.<br><br>
* 반환값: orphaned 거부 요청 결과 목록. 로그 동작: 없음.<br>
*/
fun consumeOrphanedDeniedResults(): List<OrphanedDeniedRequestResult> {
check(::permissionRequester.isInitialized) {
"PermissionRequester is not initialized. Please call super.onCreate() first."
}
return permissionRequester.consumeOrphanedDeniedResults()
}
}