안드로이드 코틀린

[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>