diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt index e2f9f2d9..c33f0862 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt @@ -182,6 +182,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB private var isHost = false private var isStaff = false + private var isCaptureRecordingAvailable = false private var isAvailableLikeHeart = false private var buttonPosition = IntArray(2) @@ -420,7 +421,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB ) // 포그라운드 진입 시 API 레벨별 캡처/녹화 감지를 시작한다. - syncCaptureSecurityPolicyByRole() + syncCaptureSecurityPolicy() if (this::layoutManager.isInitialized) { layoutManager.scrollToPosition(chatAdapter.itemCount - 1) @@ -1295,8 +1296,9 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } + isCaptureRecordingAvailable = response.isCaptureRecordingAvailable syncRoomRoleState(response) - syncCaptureSecurityPolicyByRole() + syncCaptureSecurityPolicy() binding.tvChatFreezeSwitch.visibility = if (isHost) { View.VISIBLE } else { @@ -1658,8 +1660,12 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB return isHost || isStaff } - private fun syncCaptureSecurityPolicyByRole() { - if (hasCapturePermissionByRole()) { + private fun hasCapturePermissionByPolicy(): Boolean { + return isCaptureRecordingAvailable || hasCapturePermissionByRole() + } + + private fun syncCaptureSecurityPolicy() { + if (hasCapturePermissionByPolicy()) { window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) unregisterScreenRecordingCallback() clearCapturePrivacyMuteState() @@ -1732,7 +1738,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } private fun syncCapturePrivacyMuteState() { - val shouldMute = !hasCapturePermissionByRole() && isScreenRecordingActive + val shouldMute = !hasCapturePermissionByPolicy() && isScreenRecordingActive if (isCapturePrivacyMuted == shouldMute) { return } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomRequest.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomRequest.kt index fa2ff548..4a920557 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomRequest.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/CreateLiveRoomRequest.kt @@ -22,5 +22,6 @@ data class CreateLiveRoomRequest( @SerializedName("menuPanId") val menuPanId: Long = 0, @SerializedName("menuPan") val menuPan: String = "", @SerializedName("isActiveMenuPan") val isActiveMenuPan: Boolean = false, - @SerializedName("isAvailableJoinCreator") val isAvailableJoinCreator: Boolean = true + @SerializedName("isAvailableJoinCreator") val isAvailableJoinCreator: Boolean = true, + @SerializedName("isCaptureRecordingAvailable") val isCaptureRecordingAvailable: Boolean = false ) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt index 7755f471..d33e6530 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateActivity.kt @@ -370,6 +370,14 @@ class LiveRoomCreateActivity : BaseActivity( binding.llAvailableJoinCreatorN.setOnClickListener { viewModel.setAvailableJoinCreator(false) } + + binding.llCaptureRecordingAvailableY.setOnClickListener { + viewModel.setCaptureRecordingAvailable(true) + } + + binding.llCaptureRecordingAvailableN.setOnClickListener { + viewModel.setCaptureRecordingAvailable(false) + } } @SuppressLint("SetTextI18n") @@ -582,6 +590,46 @@ class LiveRoomCreateActivity : BaseActivity( } } + viewModel.isCaptureRecordingAvailableLiveData.observe(this) { + if (it) { + binding.ivCaptureRecordingAvailableN.visibility = View.GONE + binding.llCaptureRecordingAvailableN.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b) + binding.tvCaptureRecordingAvailableN.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_3bb9f1 + ) + ) + + binding.ivCaptureRecordingAvailableY.visibility = View.VISIBLE + binding.llCaptureRecordingAvailableY.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1) + binding.tvCaptureRecordingAvailableY.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + } else { + binding.ivCaptureRecordingAvailableY.visibility = View.GONE + binding.llCaptureRecordingAvailableY.setBackgroundResource(R.drawable.bg_round_corner_6_7_13181b) + binding.tvCaptureRecordingAvailableY.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_3bb9f1 + ) + ) + + binding.ivCaptureRecordingAvailableN.visibility = View.VISIBLE + binding.llCaptureRecordingAvailableN.setBackgroundResource(R.drawable.bg_round_corner_6_7_3bb9f1) + binding.tvCaptureRecordingAvailableN.setTextColor( + ContextCompat.getColor( + applicationContext, + R.color.color_eeeeee + ) + ) + } + } + if (shouldShowAdultRestrictionSetting()) { binding.llAgeAll.setOnClickListener { viewModel.setAdult(false) diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt index e9129f9e..56158096 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/create/LiveRoomCreateViewModel.kt @@ -88,6 +88,10 @@ class LiveRoomCreateViewModel( val isAvailableJoinCreatorLiveData: LiveData get() = _isAvailableJoinCreatorLiveData + private val _isCaptureRecordingAvailableLiveData = MutableLiveData(false) + val isCaptureRecordingAvailableLiveData: LiveData + get() = _isCaptureRecordingAvailableLiveData + private val _menuLiveData = MutableLiveData("") val menuLiveData: LiveData get() = _menuLiveData @@ -169,7 +173,8 @@ class LiveRoomCreateViewModel( "" }, isActiveMenuPan = _isActivateMenuLiveData.value!!, - isAvailableJoinCreator = _isAvailableJoinCreatorLiveData.value!! + isAvailableJoinCreator = _isAvailableJoinCreatorLiveData.value!!, + isCaptureRecordingAvailable = _isCaptureRecordingAvailableLiveData.value!! ) val requestJson = Gson().toJson(request) @@ -296,6 +301,10 @@ class LiveRoomCreateViewModel( _isAvailableJoinCreatorLiveData.value = isAvailableJoinCreator } + fun setCaptureRecordingAvailable(isCaptureRecordingAvailable: Boolean) { + _isCaptureRecordingAvailableLiveData.value = isCaptureRecordingAvailable + } + fun getRecentInfo(onSuccess: (GetRecentRoomInfoResponse) -> Unit) { _isLoading.value = true compositeDisposable.add( diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt index 156ccfae..a3b2f2c3 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/info/GetRoomInfoResponse.kt @@ -27,6 +27,7 @@ data class GetRoomInfoResponse( @SerializedName("menuPan") val menuPan: String, @SerializedName("creatorLanguageCode") val creatorLanguageCode: String?, @SerializedName("isActiveRoulette") val isActiveRoulette: Boolean, + @SerializedName("isCaptureRecordingAvailable") val isCaptureRecordingAvailable: Boolean = false, @SerializedName("isChatFrozen") val isChatFrozen: Boolean = false, @SerializedName("isPrivateRoom") val isPrivateRoom: Boolean, @SerializedName("password") val password: String? = null diff --git a/app/src/main/res/layout/activity_live_room_create.xml b/app/src/main/res/layout/activity_live_room_create.xml index 0712d53d..6bc2a6db 100644 --- a/app/src/main/res/layout/activity_live_room_create.xml +++ b/app/src/main/res/layout/activity_live_room_create.xml @@ -765,6 +765,82 @@ + + + + + + + + + + + + + + + + + + + Creator entry Allowed Not allowed + Capture/recording setting Age limit All ages Gender restriction diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 2f1cbb65..811d5cc4 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -635,6 +635,7 @@ クリエイター入室設定 可能 不可 + キャプチャ/録画設定 年齢制限 全年齢 性別制限 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 88f3133a..3bfae0d0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -634,6 +634,7 @@ 크리에이터 입장 설정 가능 불가능 + 캡쳐/녹화 설정 연령 제한 전체 연령 성별 제한 diff --git a/docs/20260330_라이브캡쳐녹화가능여부반영.md b/docs/20260330_라이브캡쳐녹화가능여부반영.md new file mode 100644 index 00000000..1b168ea1 --- /dev/null +++ b/docs/20260330_라이브캡쳐녹화가능여부반영.md @@ -0,0 +1,33 @@ +# 20260330 라이브 캡쳐/녹화 가능여부 반영 + +## 구현 체크리스트 +- [x] 라이브 정보 응답 모델(`GetRoomInfoResponse`)에 `isCaptureRecordingAvailable` 필드를 추가한다. + - QA: `GetRoomInfoResponse` 역직렬화 시 필드가 누락되어도 기본값으로 동작한다. +- [x] `LiveRoomActivity`의 캡쳐/녹화 정책을 `isCaptureRecordingAvailable || isHost || isStaff` 기준으로 적용한다. + - QA: 정책 함수와 `FLAG_SECURE`/녹화 콜백/강제 음소거 계산이 동일 기준으로 동작한다. +- [x] 라이브 생성 경로(`LiveRoomCreateActivity`, `LiveRoomCreateViewModel`, `CreateLiveRoomRequest`)에 설정 UI/상태/요청 필드를 추가한다. + - QA: 생성 화면 선택값이 `CreateLiveRoomRequest.isCaptureRecordingAvailable`로 전송된다. +- [x] 라이브 수정 경로에서는 해당 설정을 변경하지 않도록 유지한다(생성 시에만 설정). + - QA: `LiveRoomInfoEditDialog`/`EditLiveRoomInfoRequest`에 신규 항목을 추가하지 않는다. +- [x] 변경 파일에 대해 정적 진단/테스트/빌드를 수행하고 결과를 기록한다. + - QA: `lsp_diagnostics` 오류 0, 관련 테스트 및 빌드 명령 성공. + +## 검증 기록 +- `lsp_diagnostics` (`.kt`) + - 무엇/왜/어떻게: 수정한 Kotlin 파일 정적 진단을 도구로 확인해 타입/문법 오류를 사전 점검했다. + - 실행 명령: `lsp_diagnostics(filePath=, severity="all")` + - 결과: 현재 실행 환경에 Kotlin LSP가 없어(`No LSP server configured for extension: .kt`) 도구 기반 진단을 수행할 수 없었다. +- Ktlint + 테스트 + 빌드 1차 + - 무엇/왜/어떻게: 코드 스타일/단위 테스트/디버그 빌드를 한 번에 검증해 회귀 가능성을 확인했다. + - 실행 명령: `./gradlew :app:ktlintCheck :app:testDebugUnitTest :app:assembleDebug` + - 결과: `:app:ktlintMainSourceSetCheck` 실패. 기존 `LiveRoomActivity.kt` 전역 스타일 위반(다수 라인)과 기존 `LiveRoomCreateViewModel.kt` unused import 경고로 실패했고, 이번 변경 기능 자체 컴파일/테스트 실패는 아님. +- 테스트 + 빌드 2차 + - 무엇/왜/어떻게: 스타일 태스크 제외 후 기능 반영 코드가 실제로 컴파일/테스트를 통과하는지 재검증했다. + - 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 결과: `BUILD SUCCESSFUL`. +- 수동 기능 검증(정책 반영 범위 확인) + - 무엇/왜/어떻게: 생성 전용 제약과 라이브룸 정책 반영 범위를 텍스트 검색으로 직접 확인했다. + - 실행 명령: + - `grep("isCaptureRecordingAvailable", app/src/main/java/**/*.kt)` + - `grep("isCaptureRecordingAvailable", app/src/main/java/.../live/room/update/*.kt)` + - 결과: 신규 필드는 `GetRoomInfoResponse`, `CreateLiveRoomRequest`, `LiveRoomCreateViewModel`, `LiveRoomCreateActivity`, `LiveRoomActivity`에만 존재하며, 수정 경로(`live/room/update`)에는 미추가로 확인됨.