Files
2nd/10_Wiki/Topics/Coding/Android_Foreground_Service_Patterns.md
T
2026-05-09 21:08:02 +09:00

5.8 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
android-foreground-service-patterns Android Foreground Service — 14+ 제약 + 타입 Coding draft B conceptual 2026-05-09 2026-05-09
android
foreground-service
vibe-coding
language applicable_to
Kotlin / Android
Android
foreground service
FGS
service type
partial wake lock
notification channel

Foreground Service

백그라운드에서 사용자가 인지하는 작업 (재생, 운동, 위치). Android 14+ = service type 명시 필수. Notification 항상 + 적절한 lifetime 만.

📖 핵심 개념

  • FGS: 사용자가 알고 있는 background 작업. notification 표시.
  • Type: dataSync / mediaPlayback / location / phoneCall / health 등 (Android 14+).
  • WorkManager: 단순 정기 작업 — FGS 보다 우선 고려.
  • 권한 (Android 13+): POST_NOTIFICATIONS.

💻 코드 패턴

Manifest

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> <!-- 14+ -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<service
    android:name=".PlayerService"
    android:foregroundServiceType="mediaPlayback"
    android:exported="false" />

Service 구현

class PlayerService : Service() {
    override fun onCreate() {
        super.onCreate()
        createChannel()
        startForeground(NOTIF_ID, buildNotification("Loading..."), 
            ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            "PLAY"  -> player.play()
            "PAUSE" -> player.pause()
            "STOP"  -> stopSelf()
        }
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onDestroy() {
        player.release()
        super.onDestroy()
    }

    private fun createChannel() {
        val ch = NotificationChannel("player", "Player", NotificationManager.IMPORTANCE_LOW)
        getSystemService<NotificationManager>()?.createNotificationChannel(ch)
    }

    private fun buildNotification(text: String): Notification {
        val pi = PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java),
            PendingIntent.FLAG_IMMUTABLE)
        return NotificationCompat.Builder(this, "player")
            .setContentTitle("Now playing")
            .setContentText(text)
            .setSmallIcon(R.drawable.ic_play)
            .setContentIntent(pi)
            .addAction(R.drawable.ic_pause, "Pause", pendingActionIntent("PAUSE"))
            .setOngoing(true)
            .build()
    }

    private fun pendingActionIntent(action: String): PendingIntent {
        val i = Intent(this, PlayerService::class.java).setAction(action)
        return PendingIntent.getService(this, action.hashCode(), i, PendingIntent.FLAG_IMMUTABLE)
    }
}

시작

val i = Intent(context, PlayerService::class.java).setAction("PLAY")
ContextCompat.startForegroundService(context, i)

⚠️ Android 12+: startForegroundService 후 5초 안에 startForeground() 호출 안 하면 ANR.

권한 요청 (notification, 13+)

if (Build.VERSION.SDK_INT >= 33) {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) 
        != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
    }
}

Type 별 권한 (14+)

mediaPlayback     → FOREGROUND_SERVICE_MEDIA_PLAYBACK
location          → FOREGROUND_SERVICE_LOCATION + ACCESS_FINE_LOCATION
camera            → FOREGROUND_SERVICE_CAMERA + CAMERA
microphone        → FOREGROUND_SERVICE_MICROPHONE + RECORD_AUDIO
dataSync          → FOREGROUND_SERVICE_DATA_SYNC (제약 강 — 6h/day 한도)
mediaProjection   → FOREGROUND_SERVICE_MEDIA_PROJECTION
phoneCall         → FOREGROUND_SERVICE_PHONE_CALL
health            → FOREGROUND_SERVICE_HEALTH (운동 등)

dataSync 6시간 제한

14+ : dataSync FGS 는 24시간 동안 6시간 한도.
초과 시 자동 stop.
대안: WorkManager + JobScheduler 짧은 작업으로 분할.

동기 stop

stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()

Compose 시작 / 중지 controls

@Composable
fun PlayerControls() {
    val ctx = LocalContext.current
    Row {
        Button(onClick = {
            ContextCompat.startForegroundService(ctx, Intent(ctx, PlayerService::class.java).setAction("PLAY"))
        }) { Text("Play") }
        Button(onClick = {
            ctx.startService(Intent(ctx, PlayerService::class.java).setAction("STOP"))
        }) { Text("Stop") }
    }
}

🤔 의사결정 기준

작업 도구
미디어 재생 FGS mediaPlayback + MediaSession
운동 추적 FGS health
짧은 sync (<10분) WorkManager
정기 sync (15분+) PeriodicWorkRequest
즉시 + 짧음 OneTimeWorkRequest
위치 추적 FGS location
화면 녹화 FGS mediaProjection

안티패턴

  • FGS 로 데이터 sync 6시간 초과: stop. WorkManager 으로.
  • 5초 안 startForeground 누락: ANR + crash.
  • Type 명시 누락 (14+): SecurityException.
  • Notification importance 높음: 알람음 매번. LOW 디폴트.
  • 사용자가 모르는 작업: FGS 가 아닌 Worker.
  • Permission 없이 mic / camera FGS: SecurityException.
  • stopSelf 안 함: 영원 — 배터리 / 메모리.

🤖 LLM 활용 힌트

  • Type + 권한 (14+) 필수.
  • 5초 안 startForeground.
  • WorkManager 가 가능하면 그게 우선.

🔗 관련 문서