안드로이드 코틀린
[Kotlin][Android] 포그라운드서비스로 GPS 위치 추적하기
teamnova
2025. 8. 15. 21:59
728x90
안녕하세요.
오늘은 포그라운드서비스를 사용해서 GPS 위치추적하는 기능을 만들어보겠습니다.
1. 포그라운드 서비스란?
포그라운드 서비스는 사용자가 직접 인식할 수 있는 작업을 수행하는 서비스입니다.
일반 서비스는 백그라운드에서 실행되지만 시스템 리소스 부족 시 언제든 종료될 수 있습니다. 반면 포그라운드 서비스는 반드시 알림(Notification)을 표시해야 하며, 사용자가 앱을 종료해도 계속 실행됩니다.
포그라운드 서비스의 특징
- 시스템에 의해 강제 종료되지 않음
- 반드시 지속적인 알림 표시 필요
- 음악 재생, 파일 다운로드, GPS 추적 등에 사용
- Android 8.0(API 26) 이상에서는 startForegroundService() 사용 필수
2. 프로젝트 설정
권한 설정 (AndroidManifest.xml)
먼저 필요한 권한들을 AndroidManifest.xml에 추가해야 합니다.
<!-- 위치 권한 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<!-- 포그라운드 서비스 권한 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application>
<!-- 서비스 등록 -->
<service
android:name=".LocationTrackingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
</application>
주요 권한
| 권한 | 설명 |
| ACCESS_FINE_LOCATION | GPS 기반 정확한 위치 |
| ACCESS_COARSE_LOCATION | 네트워크 기반 대략적 위치 |
| ACCESS_BACKGROUND_LOCATION | 백그라운드 위치 접근 (Android 10+) |
| FOREGROUND_SERVICE_LOCATION | 위치 기반 포그라운드 서비스 |
3. 위치 추적 포그라운드 서비스 구현
이제 실제 GPS 위치를 추적하는 포그라운드 서비스를 만들어보겠습니다.
import android.app.*
import android.content.Context
import android.content.Intent
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import java.text.SimpleDateFormat
import java.util.*
class LocationTrackingService : Service(), LocationListener {
companion object {
const val CHANNEL_ID = "LocationTrackingChannel"
const val NOTIFICATION_ID = 1
const val ACTION_START_TRACKING = "ACTION_START_TRACKING"
const val ACTION_STOP_TRACKING = "ACTION_STOP_TRACKING"
private const val TAG = "LocationTrackingService"
// 위치 업데이트 설정
private const val MIN_TIME_BETWEEN_UPDATES = 5000L // 5초
private const val MIN_DISTANCE_CHANGE = 10f // 10미터
}
private lateinit var locationManager: LocationManager
private var isTracking = false
private var lastKnownLocation: Location? = null
private val dateFormat = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
override fun onCreate() {
super.onCreate()
createNotificationChannel()
locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
Log.d(TAG, "LocationTrackingService created")
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START_TRACKING -> startLocationTracking()
ACTION_STOP_TRACKING -> stopLocationTracking()
}
return START_STICKY // 서비스가 종료되면 시스템이 재시작
}
3.1 위치 추적 시작/중지 로직
private fun startLocationTracking() {
if (isTracking) return
val notification = createNotification("위치 추적을 시작합니다...")
startForeground(NOTIFICATION_ID, notification)
try {
// GPS와 네트워크 위치 제공자 모두 사용
if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
MIN_TIME_BETWEEN_UPDATES,
MIN_DISTANCE_CHANGE,
this
)
Log.d(TAG, "GPS provider started")
}
if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
MIN_TIME_BETWEEN_UPDATES,
MIN_DISTANCE_CHANGE,
this
)
Log.d(TAG, "Network provider started")
}
isTracking = true
getLastKnownLocation() // 마지막 알려진 위치 가져오기
} catch (e: SecurityException) {
Log.e(TAG, "Location permission not granted", e)
stopLocationTracking()
}
}
private fun stopLocationTracking() {
if (!isTracking) return
try {
locationManager.removeUpdates(this)
isTracking = false
Log.d(TAG, "Location tracking stopped")
} catch (e: SecurityException) {
Log.e(TAG, "Error stopping location updates", e)
}
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
3.2 위치 변경 감지 및 알림 업데이트
override fun onLocationChanged(location: Location) {
lastKnownLocation = location
val currentTime = dateFormat.format(Date())
val locationText = "위도: %.6f, 경도: %.6f".format(
location.latitude,
location.longitude
)
val accuracyText = "정확도: %.1fm".format(location.accuracy)
val providerText = "제공자: ${location.provider}"
val timeText = "업데이트: $currentTime"
val fullText = "$locationText\n$accuracyText | $providerText\n$timeText"
updateNotification(fullText)
Log.d(TAG, "Location updated: $locationText, Accuracy: ${location.accuracy}m")
}
3.3 알림 채널 및 알림 생성
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"위치 추적 서비스",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "GPS 기반 위치 추적을 위한 채널"
setShowBadge(false)
}
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
}
}
private fun createNotification(contentText: String): Notification {
// 앱 열기 인텐트
val openAppIntent = Intent(this, MainActivity::class.java)
val openAppPendingIntent = PendingIntent.getActivity(
this, 0, openAppIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 추적 중지 인텐트
val stopIntent = Intent(this, LocationTrackingService::class.java).apply {
action = ACTION_STOP_TRACKING
}
val stopPendingIntent = PendingIntent.getService(
this, 0, stopIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("🌍 위치 추적 중")
.setContentText(contentText)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
.setContentIntent(openAppPendingIntent)
.addAction(
android.R.drawable.ic_menu_close_clear_cancel,
"추적 중지",
stopPendingIntent
)
.setOngoing(true) // 사용자가 스와이프로 제거할 수 없음
.setAutoCancel(false)
.build()
}
4. MainActivity 구현
이제 서비스를 제어할 MainActivity를 만들어보겠습니다.
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private var isServiceRunning = false
// 권한 요청을 위한 런처들
private val requestLocationPermissions = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val fineLocationGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false
val coarseLocationGranted = permissions[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false
when {
fineLocationGranted && coarseLocationGranted -> {
Toast.makeText(this, "위치 권한이 허용되었습니다", Toast.LENGTH_SHORT).show()
checkBackgroundLocationPermission()
}
else -> {
showLocationPermissionDialog()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
setupClickListeners()
checkAllPermissions()
updateUI()
}
4.1 권한 체크 및 요청
private fun checkLocationPermissions(): Boolean {
val fineLocation = ContextCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
val coarseLocation = ContextCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_COARSE_LOCATION
) == PackageManager.PERMISSION_GRANTED
return fineLocation && coarseLocation
}
private fun requestLocationPermissions() {
requestLocationPermissions.launch(
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
)
)
}
4.2 서비스 시작/중지
private fun startLocationTracking() {
if (!isGPSEnabled()) {
showGPSDisabledDialog()
return
}
val serviceIntent = Intent(this, LocationTrackingService::class.java).apply {
action = LocationTrackingService.ACTION_START_TRACKING
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(serviceIntent)
} else {
startService(serviceIntent)
}
isServiceRunning = true
updateUI()
Toast.makeText(this, "위치 추적을 시작했습니다", Toast.LENGTH_SHORT).show()
}
private fun stopLocationTracking() {
val serviceIntent = Intent(this, LocationTrackingService::class.java).apply {
action = LocationTrackingService.ACTION_STOP_TRACKING
}
startService(serviceIntent)
isServiceRunning = false
updateUI()
Toast.makeText(this, "위치 추적을 중지했습니다", Toast.LENGTH_SHORT).show()
}
5. 레이아웃 구성
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center_horizontal">
<!-- 제목 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🌍 GPS 위치 추적"
android:textSize="28sp"
android:textStyle="bold"
android:layout_marginBottom="24dp" />
<!-- 서비스 상태 -->
<TextView
android:id="@+id/tvServiceStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🔴 위치 추적 중지"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"
android:padding="12dp" />
<!-- 추적 시작 버튼 -->
<Button
android:id="@+id/btnStartTracking"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="📍 위치 추적 시작"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"
android:backgroundTint="#4CAF50" />
<!-- 추적 중지 버튼 -->
<Button
android:id="@+id/btnStopTracking"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="⏹️ 위치 추적 중지"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="24dp"
android:backgroundTint="#F44336"
android:enabled="false" />
</LinearLayout>
</ScrollView>