diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4765f79a..81388ca0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,9 +15,6 @@
-
-
-
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 d9a1d45c..7608ba50 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
@@ -170,25 +170,15 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB
private var isMicrophoneMute = false
private var isSpeaker = false
- // 캡처/녹화 감지로 인해 강제 음소거가 필요한지 추적한다.
private var isCapturePrivacyMuted = false
private var isScreenRecordingActive = false
- private var isScreenshotMuteActive = false
// 라이프사이클 중복 호출에서 콜백 재등록을 방지한다.
- private var isScreenCaptureCallbackRegistered = false
private var isScreenRecordingCallbackRegistered = false
// API 레벨별 콜백 인스턴스를 재사용해 등록/해제 짝을 보장한다.
- private var screenCaptureCallback: Any? = null
private var screenRecordingCallback: Any? = null
- // 스크린샷 이벤트 직후 일정 시간 음소거를 유지한 뒤 자동 해제한다.
- private val clearScreenshotMuteRunnable = Runnable {
- isScreenshotMuteActive = false
- syncCapturePrivacyMuteState()
- }
-
private var isHost = false
private var isAvailableLikeHeart = false
@@ -1597,30 +1587,9 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB
}
private fun registerCaptureSecurityCallbacks() {
- // 스크린샷/녹화 감지는 지원 API 레벨이 달라 각각 분리 등록한다.
- registerScreenshotCallback()
registerScreenRecordingCallback()
}
- private fun registerScreenshotCallback() {
- // Android 14(API 34)+에서만 스크린샷 감지 콜백을 등록한다.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE || isScreenCaptureCallbackRegistered) {
- return
- }
-
- if (screenCaptureCallback == null) {
- screenCaptureCallback = ScreenCaptureCallback {
- onScreenCaptureDetected()
- }
- }
-
- registerScreenCaptureCallback(
- mainExecutor,
- screenCaptureCallback as ScreenCaptureCallback
- )
- isScreenCaptureCallbackRegistered = true
- }
-
@Suppress("UNCHECKED_CAST")
private fun registerScreenRecordingCallback() {
// Android 15(API 35)+에서만 스크린녹화 상태 콜백을 등록한다.
@@ -1650,24 +1619,10 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB
private fun unregisterCaptureSecurityCallbacks() {
// onStart에서 등록한 콜백을 반대로 해제하고 강제 mute 상태를 정리한다.
- unregisterScreenshotCallback()
unregisterScreenRecordingCallback()
clearCapturePrivacyMuteState()
}
- private fun unregisterScreenshotCallback() {
- // Android 14+에서 등록된 스크린샷 콜백만 안전하게 해제한다.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE || !isScreenCaptureCallbackRegistered) {
- return
- }
-
- val callback = screenCaptureCallback as? ScreenCaptureCallback
- if (callback != null) {
- unregisterScreenCaptureCallback(callback)
- }
- isScreenCaptureCallbackRegistered = false
- }
-
@Suppress("UNCHECKED_CAST")
private fun unregisterScreenRecordingCallback() {
// Android 15+에서 등록된 녹화 상태 콜백만 안전하게 해제한다.
@@ -1682,14 +1637,6 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB
isScreenRecordingCallbackRegistered = false
}
- private fun onScreenCaptureDetected() {
- // 스크린샷 감지 직후 짧은 윈도우 동안 강제 mute를 유지해 유출 여지를 줄인다.
- isScreenshotMuteActive = true
- syncCapturePrivacyMuteState()
- handler.removeCallbacks(clearScreenshotMuteRunnable)
- handler.postDelayed(clearScreenshotMuteRunnable, SCREEN_CAPTURE_MUTE_HOLD_MILLIS)
- }
-
private fun onScreenRecordingStateChanged(isRecording: Boolean) {
// 시스템이 알려준 녹화 가시 상태를 강제 mute 계산에 반영한다.
isScreenRecordingActive = isRecording
@@ -1698,15 +1645,12 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB
private fun clearCapturePrivacyMuteState() {
// 라이프사이클 해제 시 캡처 기반 플래그를 모두 초기화해 원복을 보장한다.
- handler.removeCallbacks(clearScreenshotMuteRunnable)
isScreenRecordingActive = false
- isScreenshotMuteActive = false
syncCapturePrivacyMuteState()
}
private fun syncCapturePrivacyMuteState() {
- // 스크린샷/녹화 감지를 하나의 강제 mute 상태로 통합한다.
- val shouldMute = isScreenRecordingActive || isScreenshotMuteActive
+ val shouldMute = isScreenRecordingActive
if (isCapturePrivacyMuted == shouldMute) {
return
}
@@ -4230,7 +4174,6 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB
companion object {
private const val NO_CHATTING_TIME = 180L
- private const val SCREEN_CAPTURE_MUTE_HOLD_MILLIS = 2_000L
var isForeground: Boolean = false
}
}
diff --git a/docs/20260324_라이브룸캡처녹화정합개선.md b/docs/20260324_라이브룸캡처녹화정합개선.md
new file mode 100644
index 00000000..aaeeedfa
--- /dev/null
+++ b/docs/20260324_라이브룸캡처녹화정합개선.md
@@ -0,0 +1,39 @@
+# 20260324 라이브룸 캡처/녹화 정합 개선
+
+## 구현 체크리스트
+- [x] 기존 구현에서 `FLAG_SECURE`와 스크린샷/녹화 콜백 결합 지점을 다시 점검한다. (QA: `LiveRoomActivity` 캡처 보안 관련 함수 흐름 확인)
+- [x] `FLAG_SECURE` 유지 기준으로 dead path인 스크린샷 콜백 경로를 제거한다. (QA: `registerScreenCaptureCallback`/관련 상태 플래그 제거 확인)
+- [x] 녹화 상태 기반 강제 mute 경로만 유지되도록 로직과 주석을 정리한다. (QA: `addScreenRecordingCallback` 경로 단일화 확인)
+- [x] Manifest에서 불필요해진 스크린샷 감지 권한을 제거한다. (QA: `DETECT_SCREEN_CAPTURE` 선언 제거 확인)
+- [x] 진단/테스트/빌드 및 수동 QA 결과를 누적 기록한다. (QA: 실행 명령과 결과 로그 확인)
+
+## 검증 기록
+- 2026-03-24
+ - 무엇: 후속 정합 개선 작업 계획 문서를 생성했다.
+ - 왜: 사용자 요청(더 나은/최신 방식 반영)에 맞춰 변경 범위와 완료 기준을 명확히 고정하기 위해서다.
+ - 어떻게: `docs/20260324_라이브룸캡처녹화정합개선.md` 파일을 생성하고 체크리스트/검증 섹션을 작성했다.
+- 2026-03-24
+ - 무엇: 스크린샷 콜백 경로를 제거하고 녹화 기반 강제 mute 경로만 유지하도록 구현을 단순화했다.
+ - 왜: `FLAG_SECURE`를 유지하는 현재 전략에서 스크린샷 콜백 기반 강제 mute는 실질 동작하지 않는 dead path였기 때문이다.
+ - 어떻게:
+ - `app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt`에서 `registerScreenCaptureCallback`/`unregisterScreenCaptureCallback` 관련 함수, 스크린샷 상태 플래그(`isScreenshotMuteActive`) 및 타이머 상수/러너블을 제거했다.
+ - 강제 mute 계산을 `isScreenRecordingActive` 단일 상태로 정리했다.
+- 2026-03-24
+ - 무엇: Manifest에서 불필요 권한을 제거했다.
+ - 왜: 스크린샷 콜백 경로를 제거했으므로 `DETECT_SCREEN_CAPTURE` 권한이 더 이상 필요하지 않다.
+ - 어떻게: `app/src/main/AndroidManifest.xml`에서 `` 선언을 삭제하고 `DETECT_SCREEN_RECORDING`만 유지했다.
+- 2026-03-24
+ - 무엇: 설계 근거와 실행 검증을 완료했다.
+ - 왜: 변경이 실제로 dead path 제거 + 기존 보안 전략 유지를 만족하는지 객관적으로 확인하기 위해서다.
+ - 어떻게:
+ - 외부 근거 확인:
+ - Android `Activity.ScreenCaptureCallback` 문구 "This is not invoked if the activity window has WindowManager.LayoutParams.FLAG_SECURE set."를 근거로 스크린샷 콜백 비동작 조건을 재확인했다.
+ - `WindowManager` API 레퍼런스(`addScreenRecordingCallback`, `SCREEN_RECORDING_STATE_VISIBLE`, Added in API level 35)와 `WindowManager.LayoutParams.FLAG_SECURE` 레퍼런스를 확인해 녹화 상태 콜백/보안 플래그의 현재 문서 기준을 재점검했다.
+ - 정적 진단:
+ - `lsp_diagnostics` 결과: `.kt`/`.xml` LSP 서버 미구성으로 진단 불가, `.md` 파일은 diagnostics 없음.
+ - 빌드/테스트:
+ - 실행 명령: `./gradlew :app:testDebugUnitTest :app:assembleDebug`
+ - 결과: `BUILD SUCCESSFUL`
+ - 수동 QA:
+ - 실행 명령: `python3` 검증 스크립트(FLAG_SECURE 유지, 스크린샷 API 제거, recording-only mute 계산, Manifest 권한 정합 확인)
+ - 결과: `MANUAL QA PASS: FLAG_SECURE 유지 + dead screenshot path 제거 + recording-only mute flow verified.`