feat(live-room): 라이브 캡쳐 녹화 허용 설정을 생성 및 시청 정책에 반영한다

This commit is contained in:
2026-03-30 21:50:28 +09:00
parent 2d97328eb7
commit 96b385342a
10 changed files with 184 additions and 7 deletions

View File

@@ -182,6 +182,7 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(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<ActivityLiveRoomBinding>(ActivityLiveRoomB
)
// 포그라운드 진입 시 API 레벨별 캡처/녹화 감지를 시작한다.
syncCaptureSecurityPolicyByRole()
syncCaptureSecurityPolicy()
if (this::layoutManager.isInitialized) {
layoutManager.scrollToPosition(chatAdapter.itemCount - 1)
@@ -1295,8 +1296,9 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
}
isCaptureRecordingAvailable = response.isCaptureRecordingAvailable
syncRoomRoleState(response)
syncCaptureSecurityPolicyByRole()
syncCaptureSecurityPolicy()
binding.tvChatFreezeSwitch.visibility = if (isHost) {
View.VISIBLE
} else {
@@ -1658,8 +1660,12 @@ class LiveRoomActivity : BaseActivity<ActivityLiveRoomBinding>(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<ActivityLiveRoomBinding>(ActivityLiveRoomB
}
private fun syncCapturePrivacyMuteState() {
val shouldMute = !hasCapturePermissionByRole() && isScreenRecordingActive
val shouldMute = !hasCapturePermissionByPolicy() && isScreenRecordingActive
if (isCapturePrivacyMuted == shouldMute) {
return
}

View File

@@ -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
)

View File

@@ -370,6 +370,14 @@ class LiveRoomCreateActivity : BaseActivity<ActivityLiveRoomCreateBinding>(
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<ActivityLiveRoomCreateBinding>(
}
}
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)

View File

@@ -88,6 +88,10 @@ class LiveRoomCreateViewModel(
val isAvailableJoinCreatorLiveData: LiveData<Boolean>
get() = _isAvailableJoinCreatorLiveData
private val _isCaptureRecordingAvailableLiveData = MutableLiveData(false)
val isCaptureRecordingAvailableLiveData: LiveData<Boolean>
get() = _isCaptureRecordingAvailableLiveData
private val _menuLiveData = MutableLiveData("")
val menuLiveData: LiveData<String>
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(

View File

@@ -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

View File

@@ -765,6 +765,82 @@
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="33.3dp"
android:fontFamily="@font/bold"
android:lineSpacingExtra="5sp"
android:text="@string/screen_live_room_create_capture_recording_label"
android:textColor="@color/color_eeeeee"
android:textSize="16.7sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="13.3dp"
android:layout_marginTop="13.3dp"
android:baselineAligned="false">
<LinearLayout
android:id="@+id/ll_capture_recording_available_y"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_13181b"
android:gravity="center"
android:paddingVertical="14.3dp">
<ImageView
android:id="@+id/iv_capture_recording_available_y"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6.7dp"
android:contentDescription="@null"
android:src="@drawable/ic_select_check"
android:visibility="gone" />
<TextView
android:id="@+id/tv_capture_recording_available_y"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/bold"
android:text="@string/screen_live_room_create_creator_join_available"
android:textColor="@color/color_3bb9f1"
android:textSize="14.7sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_capture_recording_available_n"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="13.3dp"
android:layout_weight="1"
android:background="@drawable/bg_round_corner_6_7_13181b"
android:gravity="center"
android:paddingVertical="14.3dp">
<ImageView
android:id="@+id/iv_capture_recording_available_n"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6.7dp"
android:contentDescription="@null"
android:src="@drawable/ic_select_check"
android:visibility="gone" />
<TextView
android:id="@+id/tv_capture_recording_available_n"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="@font/bold"
android:text="@string/screen_live_room_create_creator_join_unavailable"
android:textColor="@color/color_3bb9f1"
android:textSize="14.7sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_set_adult"
android:layout_width="match_parent"

View File

@@ -635,6 +635,7 @@
<string name="screen_live_room_create_creator_join_label">Creator entry</string>
<string name="screen_live_room_create_creator_join_available">Allowed</string>
<string name="screen_live_room_create_creator_join_unavailable">Not allowed</string>
<string name="screen_live_room_create_capture_recording_label">Capture/recording setting</string>
<string name="screen_live_room_create_age_label">Age limit</string>
<string name="screen_live_room_create_age_all">All ages</string>
<string name="screen_live_room_create_gender_restriction_label">Gender restriction</string>

View File

@@ -635,6 +635,7 @@
<string name="screen_live_room_create_creator_join_label">クリエイター入室設定</string>
<string name="screen_live_room_create_creator_join_available">可能</string>
<string name="screen_live_room_create_creator_join_unavailable">不可</string>
<string name="screen_live_room_create_capture_recording_label">キャプチャ/録画設定</string>
<string name="screen_live_room_create_age_label">年齢制限</string>
<string name="screen_live_room_create_age_all">全年齢</string>
<string name="screen_live_room_create_gender_restriction_label">性別制限</string>

View File

@@ -634,6 +634,7 @@
<string name="screen_live_room_create_creator_join_label">크리에이터 입장 설정</string>
<string name="screen_live_room_create_creator_join_available">가능</string>
<string name="screen_live_room_create_creator_join_unavailable">불가능</string>
<string name="screen_live_room_create_capture_recording_label">캡쳐/녹화 설정</string>
<string name="screen_live_room_create_age_label">연령 제한</string>
<string name="screen_live_room_create_age_all">전체 연령</string>
<string name="screen_live_room_create_gender_restriction_label">성별 제한</string>

View File

@@ -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=<modified .kt>, 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`)에는 미추가로 확인됨.