Android 12에서 ACTION_CLOSE_SYSTEM_DIALOGS가 막혔을 때: SecurityException 원인과 안전한 대체 방법

Prompt Architect 편집팀 · 2026-06-18 · 7분

TL;DR — Android 12(API 31~32)부터 ACTION_CLOSE_SYSTEM_DIALOGS를 호출하면 SecurityException이 발생합니다. 왜 차단됐는지, 그리고 startActivity 기반으로 어떻게 안전하게 대체할지 단계별로 정리했습니다.

알림 패널이나 시스템 다이얼로그를 닫기 위해 오래전부터 써 오던 Intent.ACTION_CLOSE_SYSTEM_DIALOGS. 그런데 앱의 targetSdkVersion을 31 이상(Android 12)으로 올리는 순간, 평소 잘 동작하던 이 코드가 갑자기 앱을 죽이기 시작합니다. 로그캣에는 낯선 SecurityException이 찍히고, 이전 버전에서는 멀쩡하던 기능이 특정 기기에서만 크래시를 내죠.

이 글에서는 이 변화의 배경을 짚고, 기존 코드를 어떻게 안전하게 바꿔야 하는지, 그리고 실무에서 자주 놓치는 함정까지 정리합니다.

1. 어떤 상황에서 터지는가

문제가 되는 전형적인 코드는 다음과 같은 형태입니다. 알림 영역에서 사용자가 항목을 탭하면 액티비티를 띄우기 전에 펼쳐진 알림 그림자(notification shade)를 닫으려는 의도죠.

// ❌ Android 12(API 31)부터 SecurityException 발생
val closeIntent = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
context.sendBroadcast(closeIntent)

targetSdkVersion이 30 이하라면 무시되거나 경고 수준으로 끝나지만, 31 이상으로 빌드한 앱이 일반 권한만 가진 상태에서 이 브로드캐스트를 보내면 시스템이 다음과 같이 거부합니다.

java.lang.SecurityException:
  com.example.myapp attempted to send the broadcast action
  android.intent.action.CLOSE_SYSTEM_DIALOGS
  but is not allowed to do so.

즉, 컴파일 단계의 오류가 아니라 런타임에서 차단되기 때문에, 정적 분석이나 빌드만으로는 놓치기 쉽습니다. QA 단계나 출시 후 크래시 리포트에서 처음 발견되는 경우가 많습니다.

2. Android 12가 이 동작을 막은 이유

ACTION_CLOSE_SYSTEM_DIALOGS는 원래 알림 그림자, 최근 앱 화면, 시스템 다이얼로그 등을 강제로 닫는 강력한 신호입니다. 문제는 이게 너무 강력했다는 점입니다.

악성 앱이 이 인텐트를 악용하면, 사용자가 보안 경고나 권한 요청 다이얼로그를 읽거나 확인하려는 순간에 화면을 강제로 닫아버려 사용자의 판단을 방해할 수 있습니다. 일종의 UI 가로채기(클릭재킹과 유사한 회피) 수단으로 쓰일 여지가 있었던 거죠.

그래서 Android 12에서는 "동작 변경사항: 모든 앱(Behavior changes: all apps)" 정책의 일부로, 일반 앱이 이 인텐트를 보내는 것을 막았습니다. 보안 강화가 핵심 동기이며, 같은 맥락에서 Android 12는 PendingIntent mutability 명시 의무화, 정확한 알람(exact alarm) 권한 분리 등 여러 제약을 함께 도입했습니다.

3. 핵심 해결책: startActivity가 알아서 닫는다

가장 중요한 사실은, 대부분의 경우 이 인텐트가 애초에 필요 없었다는 것입니다.

기존 코드의 의도는 보통 "알림을 탭해서 액티비티를 열기 전에 알림 패널을 접는다"였습니다. 그런데 안드로이드는 새로운 액티비티를 전면으로 띄울 때 펼쳐진 시스템 패널을 자동으로 닫아줍니다. 따라서 별도의 close 인텐트 없이 그냥 화면을 열면 됩니다.

// ✅ 권장: 별도 close 인텐트 없이 액티비티만 시작하면 패널이 자동으로 닫힘
val targetIntent = Intent(context, DetailActivity::class.java).apply {
    flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
context.startActivity(targetIntent)

PendingIntent로 알림을 구성하는 경우에도 동일합니다. 알림 탭 시 실행될 PendingIntent가 액티비티를 시작하도록 만들어 두면, 시스템이 알림 그림자를 알아서 정리합니다.

// ✅ 알림에서 액티비티로 이동 — 별도 dialog close 불필요
val detailIntent = Intent(context, DetailActivity::class.java)
val pendingIntent = PendingIntent.getActivity(
    context,
    REQUEST_CODE,
    detailIntent,
    // Android 12부터 mutability 플래그 명시 필수
    PendingIntent.FLAG_IMMUTABLE
)

val notification = NotificationCompat.Builder(context, CHANNEL_ID)
    .setContentTitle("새 메시지")
    .setContentText("탭하면 상세 화면으로 이동합니다")
    .setSmallIcon(R.drawable.ic_notification)
    .setContentIntent(pendingIntent) // 탭 → 액티비티 시작 → 패널 자동 닫힘
    .setAutoCancel(true)
    .build()

4. 버전 분기 처리 예제

레거시 코드와의 호환성을 유지하면서 점진적으로 마이그레이션하고 싶다면, OS 버전으로 분기할 수 있습니다. 단, 새로 작성하는 코드라면 분기 없이 startActivity 패턴으로 통일하는 편이 훨씬 깔끔합니다.

fun dismissSystemPanelAndOpen(context: Context, target: Intent) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
        // Android 11 이하: 구식 방식이 여전히 동작 (필요할 때만)
        @Suppress("DEPRECATION")
        context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
    }
    // Android 12 이상: 인텐트를 보내지 않는다. 액티비티 시작만으로 충분
    target.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    context.startActivity(target)
}

Build.VERSION_CODES.S가 곧 API 31(Android 12)입니다. 이 분기는 어디까지나 과도기용이며, 장기적으로는 close 브로드캐스트 호출 자체를 제거하는 것이 정답입니다.

5. 시스템 앱이라면 예외가 있다

이 차단은 일반(서드파티) 앱에 적용됩니다. 플랫폼 서명을 가진 시스템 앱이나 특정 시스템 권한을 보유한 앱은 여전히 이 인텐트를 사용할 수 있습니다. 관련 권한은 다음과 같습니다.

<!-- 시스템 서명 앱에서만 의미 있음 — 일반 앱은 부여받지 못함 -->
<uses-permission
    android:name="android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS" />

즉, "권한만 추가하면 되지 않나?"라고 생각해서 매니페스트에 위 권한을 넣어도, 일반 배포 앱에서는 부여되지 않으므로 여전히 막힙니다. 런처, OEM 기본 앱, MDM 솔루션처럼 플랫폼 서명을 받는 특수한 경우가 아니라면 이 길은 막혀 있다고 보면 됩니다.

6. 흔히 빠지는 함정과 점검 포인트

  • 라이브러리 안에 숨어 있는 호출: 직접 작성한 코드가 아니라 외부 SDK(광고, 분석, 푸시 라이브러리 등)가 내부적으로 이 브로드캐스트를 보내는 경우가 있습니다. 크래시 스택을 끝까지 따라가 보세요.
  • sendBroadcast뿐 아니라 인텐트 생성 자체를 검색: 코드베이스에서 CLOSE_SYSTEM_DIALOGS 문자열을 전수 검색(grep)해 누락을 막습니다.
  • PendingIntent mutability 누락 동반 발생: Android 12로 올리면 close 인텐트 문제와 PendingIntent.FLAG_IMMUTABLE/FLAG_MUTABLE 미지정 크래시가 함께 터지는 경우가 많습니다. 두 가지를 같이 점검하세요.
  • 실제 12 기기/에뮬레이터에서 테스트: 빌드만으로는 잡히지 않는 런타임 예외이므로, API 31~34 환경에서 실제 알림 탭 흐름을 반드시 검증해야 합니다.
  • 알림이 아닌 다른 용도였는지 확인: 알림 외에 "다른 앱의 다이얼로그를 닫으려는" 의도였다면 그건 처음부터 권장되지 않는 패턴입니다. 사용자 흐름을 재설계하는 것이 맞습니다.

7. 요약

  • Android 12(API 31)부터 일반 앱의 ACTION_CLOSE_SYSTEM_DIALOGS 브로드캐스트는 SecurityException으로 차단된다.
  • 차단 이유는 보안 — 악용 시 사용자의 보안 판단을 가로챌 수 있기 때문이다.
  • 해결책은 단순하다. 인텐트를 보내지 말고 그냥 startActivity()(또는 액티비티용 PendingIntent)로 화면을 열면 시스템 패널이 자동으로 닫힌다.
  • 과도기에는 Build.VERSION_CODES.S 기준으로 분기할 수 있지만, 최종적으로는 close 호출을 제거하는 것이 정답이다.
  • 시스템 서명 앱만 예외이며, 매니페스트 권한 추가로는 일반 앱에서 우회되지 않는다.

AI에게 물어볼 때 (프롬프트 팁)

이런 OS 동작 변경 이슈는 AI에게 물어볼 때 버전·맥락·의도를 명확히 주면 훨씬 정확한 답을 얻습니다. Prompt Architect에서 권장하는 구체 프롬프트 예시입니다.

  1. 원인 진단형

    "Android 12(targetSdkVersion 32) 앱에서 Intent.ACTION_CLOSE_SYSTEM_DIALOGS를 sendBroadcast 했더니 SecurityException이 난다. 왜 막혔는지 보안 관점에서 설명하고, 알림 탭 후 액티비티를 여는 시나리오에서 권장되는 대체 코드를 Kotlin으로 보여줘. PendingIntent mutability도 함께 반영해줘."

  2. 리팩토링 검토형

    "다음 레거시 코드를 Android 12 이상에서 안전하게 동작하도록 수정해줘. 동작은 동일하게 유지하되, 차단된 API를 제거하고 버전 분기가 정말 필요한지도 판단해서 알려줘. [기존 코드 붙여넣기]"

  3. 회귀 점검형

    "내 안드로이드 코드베이스를 Android 12로 마이그레이션 중이다. CLOSE_SYSTEM_DIALOGS 외에 일반 앱에서 깨질 수 있는 '동작 변경사항: 모든 앱' 항목을 체크리스트로 정리하고, 각 항목의 탐지 방법(grep 키워드, 로그 패턴)도 같이 제시해줘."

좋은 프롬프트는 "정확한 버전 + 실제 에러 메시지 + 코드 + 원하는 출력 형식"을 함께 담습니다. 이 네 가지를 갖추면 AI 답변의 재현성과 정확도가 크게 올라갑니다.