[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
---
|
||||
id: android-foreground-service-patterns
|
||||
title: Android Foreground Service — 14+ 제약 + 타입
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [android, foreground-service, vibe-coding]
|
||||
tech_stack: { language: "Kotlin / Android", applicable_to: ["Android"] }
|
||||
applied_in: []
|
||||
aliases: [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
|
||||
```xml
|
||||
<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 구현
|
||||
```kotlin
|
||||
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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 시작
|
||||
```kotlin
|
||||
val i = Intent(context, PlayerService::class.java).setAction("PLAY")
|
||||
ContextCompat.startForegroundService(context, i)
|
||||
```
|
||||
|
||||
⚠️ Android 12+: `startForegroundService` 후 5초 안에 `startForeground()` 호출 안 하면 ANR.
|
||||
|
||||
### 권한 요청 (notification, 13+)
|
||||
```kotlin
|
||||
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
|
||||
```kotlin
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
stopSelf()
|
||||
```
|
||||
|
||||
### Compose 시작 / 중지 controls
|
||||
```kotlin
|
||||
@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 가 가능하면 그게 우선.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Android_WorkManager_Patterns]]
|
||||
- [[Android_Lifecycle_Aware_Components]]
|
||||
- [[Native_Battery_Network_Profiling]]
|
||||
Reference in New Issue
Block a user