diff --git a/app/build.gradle b/app/build.gradle index a99d5ee5..ddc76f98 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,7 +64,7 @@ android { minSdk 23 targetSdk 35 versionCode 233 - versionName "1.54.1" + versionName "1.54.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } 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 627ab3dd..987c92c6 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 @@ -415,7 +415,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB ) // 포그라운드 진입 시 API 레벨별 캡처/녹화 감지를 시작한다. - registerCaptureSecurityCallbacks() + syncCaptureSecurityPolicyByRole() if (this::layoutManager.isInitialized) { layoutManager.scrollToPosition(chatAdapter.itemCount - 1) @@ -1256,6 +1256,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } isHost = response.creatorId == SharedPreferenceManager.userId + syncCaptureSecurityPolicyByRole() binding.tvChatFreezeSwitch.visibility = if (isHost) { View.VISIBLE } else { @@ -1605,8 +1606,18 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB }, 100) } - private fun registerCaptureSecurityCallbacks() { - registerScreenRecordingCallback() + private fun syncCaptureSecurityPolicyByRole() { + if (isHost) { + window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE) + unregisterScreenRecordingCallback() + clearCapturePrivacyMuteState() + return + } + + window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) + if (isForeground) { + registerScreenRecordingCallback() + } } @Suppress("UNCHECKED_CAST") @@ -1669,7 +1680,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } private fun syncCapturePrivacyMuteState() { - val shouldMute = isScreenRecordingActive + val shouldMute = !isHost && isScreenRecordingActive if (isCapturePrivacyMuted == shouldMute) { return } diff --git a/docs/20260328_라이브룸방장캡쳐녹화허용.md b/docs/20260328_라이브룸방장캡쳐녹화허용.md new file mode 100644 index 00000000..5da3e33a --- /dev/null +++ b/docs/20260328_라이브룸방장캡쳐녹화허용.md @@ -0,0 +1,43 @@ +# 20260328 라이브룸 방장 캡쳐/녹화 허용 + +## 구현 체크리스트 +- [x] 방장 판별 시점과 캡처 보안 적용 지점을 확인한다. (QA: `isHost` 갱신 지점과 `FLAG_SECURE` 적용 지점 라인 확인) +- [x] 방장일 때만 `FLAG_SECURE`를 해제하고, 청취자는 기존 차단 상태를 유지한다. (QA: 방장/비방장 분기에서 `addFlags`/`clearFlags` 동작 확인) +- [x] 방장일 때 녹화 감지 기반 강제 mute가 적용되지 않도록 정합을 맞춘다. (QA: `syncCapturePrivacyMuteState` 분기 및 콜백 등록/해제 흐름 확인) +- [x] 진단/빌드/테스트/수동 QA를 수행하고 결과를 기록한다. (QA: 실행 명령과 결과 로그 확인) + +## 검증 기록 +- 2026-03-28 + - 무엇: 방장 예외 적용을 위한 코드베이스/외부 레퍼런스 병렬 탐색을 수행했다. + - 왜: `FLAG_SECURE`를 역할 기반으로 런타임 토글할 때 라이프사이클/콜백 경합 없이 최소 변경으로 구현하기 위해서다. + - 어떻게: + - 내부 탐색(`explore`): + - `bg_016c0dfd` (host 보안 플로우 맵) + - `bg_ba4aa673` (host 판별 지연 시 race 위험 분석) + - `bg_3132d80b` (저장소 내 역할 기반 secure 패턴 탐색) + - 외부 탐색(`librarian`): + - `bg_1875bb8f` (Android 공식 문서/AOSP의 addFlags/clearFlags 근거) + - `bg_d010820d` (OSS 동적 토글 사례: Fenix/Signal/Bitwarden) + - 직접 검색: + - `grep`/`ast_grep_search`/`sg run`으로 `FLAG_SECURE`, `isHost`, 콜백 등록/해제, mute 계산식을 교차 확인 + - `rg`는 로컬 환경에 설치되어 있지 않아(`command -v rg` 결과 없음) `grep`/`sg`로 대체 검증 + +- 2026-03-28 + - 무엇: `LiveRoomActivity`에 방장 전용 캡처/녹화 허용 정책을 구현했다. + - 왜: 사용자 요청대로 방장(host)은 캡처/화면녹화를 허용하고, 청취자는 기존 차단 정책을 유지해야 하기 때문이다. + - 어떻게: + - `isHost` 판별 직후 정책 동기화를 위해 `syncCaptureSecurityPolicyByRole()`를 추가했다. + - 방장(`isHost=true`): `window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)` + 녹화 콜백 해제 + 강제 mute 상태 정리 + - 청취자(`isHost=false`): `window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)` + 포그라운드에서 녹화 콜백 등록 유지 + - `viewModel.roomInfoLiveData.observe`에서 `isHost` 갱신 직후 `syncCaptureSecurityPolicyByRole()`를 호출해 비동기 roomInfo 도착 시점에도 즉시 반영되도록 했다. + - `syncCapturePrivacyMuteState()`를 `val shouldMute = !isHost && isScreenRecordingActive`로 변경해 방장은 녹화 중에도 강제 mute 대상에서 제외했다. + +- 2026-03-28 + - 무엇: 진단/빌드/테스트/수동 QA를 완료했다. + - 왜: 컴파일 안정성과 요청 동작(방장 허용, 청취자 유지)을 실제 증거로 확인하기 위해서다. + - 어떻게: + - LSP 진단: `.kt` 서버 미구성으로 `lsp_diagnostics` 불가(환경 제약 확인), `.md` 파일 diagnostics는 없음. + - 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug` + - 결과: `BUILD SUCCESSFUL` + - 수동 QA 명령: `python3` 스크립트로 정책 함수/분기/호출 순서/mute 계산식을 점검 + - 수동 QA 결과: `MANUAL_QA_PASS: host can bypass capture security while listeners remain protected in source flow` diff --git a/docs/20260328_라이브룸캡쳐녹화차단점검.md b/docs/20260328_라이브룸캡쳐녹화차단점검.md new file mode 100644 index 00000000..59c0b276 --- /dev/null +++ b/docs/20260328_라이브룸캡쳐녹화차단점검.md @@ -0,0 +1,42 @@ +# 20260328 라이브룸 캡쳐/화면녹화 차단 점검 + +## 구현/점검 체크리스트 +- [x] `LiveRoomActivity` 내 캡쳐/화면녹화 차단 적용 지점 확인 +- [x] 전체 코드베이스에서 차단 해제/우회 가능 경로(`clearFlags`, `FLAG_SECURE` 재설정 등) 탐색 +- [x] 화면녹화 감지 및 후속 처리(음소거/콜백 등록 해제) 로직 검증 +- [x] 외부 레퍼런스(Android 공식 동작)와 현재 구현 정합성 검증 +- [x] 점검 결과 및 근거(명령/파일/라인) 기록 + +## 검증 기록 +- 2026-03-28 + - 무엇: 라이브룸 캡처/화면녹화 차단 적용 여부를 코드베이스 전역과 외부 레퍼런스로 교차 점검했다. + - 왜: 사용자 질문("현재 모든 사람이 캡쳐와 화면녹화가 불가능한지")에 대해 단일 파일 확인이 아닌 우회 경로/플랫폼 제약까지 포함한 근거를 확보해야 했기 때문이다. + - 어떻게: + - 병렬 탐색(내부): `explore` 에이전트 3건 + - `bg_baa23d06` (LiveRoomActivity 보안 플로우 추적) + - `bg_c991f78f` (우회/해제 경로 탐색) + - `bg_a5c8b08e` (대체 라이브 진입 화면 탐색) + - 병렬 탐색(외부): `librarian` 에이전트 2건 + - `bg_b2336b84` (Android 공식 동작/제약) + - `bg_320d7f9b` (OSS 구현 패턴 비교) + - 직접 검색/정적 검증 명령: + - `grep "FLAG_SECURE|registerScreenRecordingCallback|addScreenRecordingCallback|removeScreenRecordingCallback"` (저장소 전역) + - `ast_grep_search`/`sg run`으로 `window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)` 및 `window.clearFlags(...)` 존재 여부 확인 + - `grep "clearFlags\(|setFlags\("` (app/src/main/java 전역) + - `read`로 `LiveRoomActivity.kt` 라이프사이클/콜백/mute 처리 라인 직접 확인 + - SDK 레퍼런스 확인: `/Users/klaus/Library/Android/sdk/platforms/android-35/data/api-versions.xml` + - 결과(핵심 근거): + - `LiveRoomActivity.kt:390`에서 `window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)`가 무조건 적용된다. + - 앱 코드에서 `window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)`는 발견되지 않았다(`sg run`/`ast_grep_search`/`grep` 교차 검증). + - 녹화 감지/후처리 로직: + - 등록: `LiveRoomActivity.kt:1627` (`windowManager.addScreenRecordingCallback`) + - 해제: `LiveRoomActivity.kt:1654` (`windowManager.removeScreenRecordingCallback`) + - 상태 반영: `LiveRoomActivity.kt:1659-1693` (`isScreenRecordingActive` -> `isCapturePrivacyMuted` -> `applyEffectiveAudioMuteState`) + - API 레벨 근거: + - `addScreenRecordingCallback`/`removeScreenRecordingCallback`/`SCREEN_RECORDING_STATE_VISIBLE`는 API 35부터(`api-versions.xml:70441,70451,70475`). + - `FLAG_SECURE`는 Android 플랫폼 상수로 존재(`api-versions.xml:70558`). + - 권한 근거: + - `AndroidManifest.xml:19`에 `` 선언이 존재한다. + - 결론: + - **LiveRoomActivity 화면에 입장한 사용자 기준으로는 캡처/녹화 노출이 차단되도록 구현되어 있다(FLAG_SECURE 적용 + 해제 경로 부재).** + - 다만 Android 공식 문서 범위상 `FLAG_SECURE`는 기본적으로 스크린샷/비보안 디스플레이 노출 차단을 보장하며, 모든 녹화 시나리오 100% 차단을 플랫폼이 절대 보장한다고 단정할 수는 없다.