기타

[Android] 화면 대기(백그라운드) 상태에서 GPS 위치 수집 및 주기적 데이터 전송 구현하기

2025. 5. 31. 19:24
목차
  1. 기능 필요 이유
  2. 플로우(검정색 글씨만 이번 글에서 표현)
  3. 실습 과정
  4. 출처

기능 필요 이유

마라톤 관련 모바일&워치 앱 프로젝트 진행중 스마트폰 GPS를 통해 사용자의 거리 측정 데이터를 저장 및 전송해야 했습니다. 관련해서 워치는 블루트스 데이터 통신이었고 DB는 Firebase Realtime Database를 사용했습니다. 5초마다 GPS거리를 기반으로 누적 뛴 거리를 저장해야하는 로직이 필요했고 이를 위해 백그라운드 상태에서도 GPS위치에 따른 거리 계산 및 데이터 전송이 진행되도록 했습니다.

 


플로우(검정색 글씨만 이번 글에서 표현)

  1. 앱 실행-> 포그라운드 서비스 시작(Notification 생성) -> 권한 검사
  2. 5초마다 GPS 워치 수신 -> 이전 워치와 비교해 거리 계산 - > 누적 거리 업데이트
  3. Firebase Realtime DB로 누적거리 전송 -> Wear OS 워치로 실시간 거리 전송(이 내용은 타 팀원 작업 예정이라 현재 글에선 제외하겠습니다.)
  4. 사용자가 "측정 종료" 시 -> 포그라운드 서비스 중지 -> 워치 업데이트 종료

실습 과정

권한, 의존성 추가

권한 추가

    <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" />

 

의존성 추가

dependencies {
    implementation("com.google.android.gms:play-services-location:21.0.1")  
    // Firebase Realtime Database
    implementation("com.google.firebase:firebase-database-ktx:20.2.2")  
}

 

Realtime DB 세팅 관련해서는 타 블로그 글이 많아서 넘어가겠습니다

1단계: 앱 실행 -> 포그라운드 서비스 시작(Notification 생성) -> 권한 검사

MainActivity.kt

package com.example.gogoma.ui.screens// 파일 위치: app/src/main/java/com/example/testapplication/ui/screens/MarathonDistanceGPSActivity.kt

import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.gogoma.services.LocationForegroundService

class MarathonDistanceGPSTestActivity : ComponentActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Foreground Service를 시작하여 백그라운드에서 5초마다 위치 업데이트 및 DB 업데이트 로직을 처리합니다.
        val serviceIntent = Intent(this, LocationForegroundService::class.java)
        startForegroundService(serviceIntent)

        // 간단한 UI를 표시합니다.
        setContent {
            MarathonDistanceGPSScreen()
        }
    }
}

@Composable
fun MarathonDistanceGPSScreen() {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(text = "위치 업데이트 서비스가 실행 중입니다.")
        Text(text = "백그라운드에서 위치 업데이트 및 DB 업데이트를 수행 중입니다. 로그를 확인하세요.")
    }

2단계: 5초마다 GPS 위치 수신 -> 누적 거리 업데이트

LocationForegroundService.kt

package com.example.gogoma.services

import android.Manifest
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.os.Build
import android.os.IBinder
import android.os.Looper
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import com.example.gogoma.data.repository.UserDistanceRepository
import com.google.android.gms.location.*

class LocationForegroundService : Service() {

    private lateinit var fusedLocationClient: FusedLocationProviderClient
    private lateinit var locationRequest: LocationRequest
    private lateinit var locationCallback: LocationCallback

    // 누적 거리 (킬로미터 단위)
    private var cumulativeDistance = 0.0
    private var previousLocation: Location? = null

    // 예시 사용자 ID (백엔드에서 auto PK로 생성된 ID; 여기서는 "1" 사용)
    private val userId = "1"

    override fun onCreate() {
        super.onCreate()
        // FusedLocationProviderClient 초기화
        fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)

        // 5초마다 위치 업데이트 요청
        locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 5000L)
            .setMinUpdateIntervalMillis(5000)
            .build()

        // 위치 업데이트 콜백 설정
        locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                for (currentLocation in locationResult.locations) {
                    Log.d("LocationForegroundService", "현재 위치 -> 위도: ${currentLocation.latitude}, 경도: ${currentLocation.longitude}")

                    // 이전 위치가 존재하면, 두 위치 사이의 거리를 계산하여 누적 거리 업데이트
                    previousLocation?.let { prevLoc ->
                        val distanceInMeters = prevLoc.distanceTo(currentLocation)
                        val incrementalDistance = distanceInMeters / 1000.0  // km 단위로 변환
                        cumulativeDistance += incrementalDistance

                        val formattedCumulative = formatDistance(cumulativeDistance)
                        val formattedIncrement = formatDistance(incrementalDistance)
                        Log.d("MarathonDistanceGPSActivity", "이번 이동 거리: $formattedIncrement km, 누적 거리: $formattedCumulative km")

                        // Firebase에 누적 거리 업데이트
                        UserDistanceRepository.updateUserCumulativeDistance(
                            userId,
                            cumulativeDistance,
                            onSuccess = {
                                Log.d("LocationForegroundService", "Firebase 업데이트 성공: $cumulativeDistance km")
                            },
                            onFailure = { exception ->
                                Log.e("LocationForegroundService", "Firebase 업데이트 실패: ${exception.message}")
                            }
                        )
                    }

                    if (previousLocation == null) {
                        Log.d("LocationForegroundService", "첫 위치 업데이트입니다.")
                    }

                    // 현재 위치를 이전 위치로 저장
                    previousLocation = currentLocation
                }
            }
        }

        // Foreground Service 시작 및 위치 업데이트 시작
        startForegroundServiceWithNotification()
        startLocationUpdates()
    }

    private fun formatDistance(distance: Double): String {
        return String.format("%.2f", distance)
    }

    private fun startForegroundServiceWithNotification() {
        val channelId = "LocationServiceChannel"
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                channelId,
                "Location Service",
                NotificationManager.IMPORTANCE_LOW
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager?.createNotificationChannel(channel)
        }
        val notification: Notification = NotificationCompat.Builder(this, channelId)
            .setContentTitle("위치 추적 중")
            .setContentText("앱이 백그라운드에서 위치를 추적합니다.")
            .setSmallIcon(android.R.drawable.ic_menu_mylocation)
            .build()
        // startForeground() 호출로 서비스가 포그라운드 서비스로 실행됨
        startForeground(1, notification)
    }

    private fun startLocationUpdates() {
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED &&
            ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
            Log.e("LocationForegroundService", "위치 권한 부족")
            return
        }
        fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
    }

    override fun onDestroy() {
        super.onDestroy()
        fusedLocationClient.removeLocationUpdates(locationCallback)
    }

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

 

3단계: Firebase Realtime DB로 누적거리 전송

UserDistanceRepository.kt

package com.example.gogoma.data.repository

import com.google.firebase.database.FirebaseDatabase

data class UserData(
    val distance: Double = 0.0,
)

object UserDistanceRepository {
    private val database = FirebaseDatabase.getInstance("https://gogomarathon-b07af-default-rtdb.asia-southeast1.firebasedatabase.app/")
    private val marathonRef = database.getReference("users")

    // 사용자의 초기 데이터를 생성하는 메서드 (예: 누적거리 0, 시작 시간 등)
    fun createInitialUserData(
        userId: String,
        onSuccess: () -> Unit,
        onFailure: (Exception) -> Unit
    ) {
        // 예시 데이터: 누적거리 0
        val initialData = UserData(distance = 0.0)
        marathonRef.child(userId).setValue(initialData)
            .addOnSuccessListener { onSuccess() }
            .addOnFailureListener { exception -> onFailure(exception) }
    }

    // 누적 거리를 조회하는 메서드
    fun getUserCumulativeDistance(userId: String, onResult: (Double?) -> Unit) {
        marathonRef.child(userId).child("distance").get()
            .addOnSuccessListener { snapshot ->
                val distance = snapshot.getValue(Double::class.java)
                onResult(distance)
            }
            .addOnFailureListener { onResult(null) }
    }

    // 누적 거리를 업데이트하는 메서드
    fun updateUserCumulativeDistance(
        userId: String,
        newCumulativeDistance: Double,
        onSuccess: () -> Unit,
        onFailure: (Exception) -> Unit
    ) {
        marathonRef.child(userId).child("distance").setValue(newCumulativeDistance)
            .addOnSuccessListener { onSuccess() }
            .addOnFailureListener { exception -> onFailure(exception) }
    }

    // (선택사항) 사용자의 전체 데이터를 삭제하는 메서드
    fun deleteUserData(
        userId: String,
        onSuccess: () -> Unit,
        onFailure: (Exception) -> Unit
    ) {
        marathonRef.child(userId).removeValue()
            .addOnSuccessListener { onSuccess() }
            .addOnFailureListener { exception -> onFailure(exception) }
    }
}

 

 


출처

https://tekken5953.tistory.com/15

 

안드로이드에서 Firebase Realtime DB 사용하기

데이터의 저장보통 우리는 어떠한 값을 저장할 때 데이터 저장소인 DB를 이용합니다.저장 하려는 데이터가 원시 데이터일 경우 안드로이드 SharedPreference API를 이용하기도 하고, 외부통신 없이 빠

tekken5953.tistory.com

 

 

 

 

저작자표시 (새창열림)

'기타' 카테고리의 다른 글

[기타] SW역량 테스트 B형 합격 후기 및 삼성 갤럭시 버즈 후기  (2) 2025.02.05
[API] 번역 API: DeepL 사용 및 Java에 적용  (0) 2025.02.02
GIT 명령어 동작원리  (0) 2024.08.16
[SSAFY 12기 전공 서울 합격 후기] 코딩 테스트, 면접 준비과정 & 졸업  (2) 2024.07.10
[POSTMAN] POSTMAN 이미지 업로드 에러  (1) 2023.08.26
  1. 기능 필요 이유
  2. 플로우(검정색 글씨만 이번 글에서 표현)
  3. 실습 과정
  4. 출처
'기타' 카테고리의 다른 글
  • [기타] SW역량 테스트 B형 합격 후기 및 삼성 갤럭시 버즈 후기
  • [API] 번역 API: DeepL 사용 및 Java에 적용
  • GIT 명령어 동작원리
  • [SSAFY 12기 전공 서울 합격 후기] 코딩 테스트, 면접 준비과정 & 졸업
Ash_jisu
Ash_jisu
Ash_jisu
JisuStory
Ash_jisu
전체
오늘
어제
  • 분류 전체보기 (136)
    • 알고리즘 (68)
      • 자바 (68)
      • C++ (0)
    • 자바(Java) (18)
    • 스프링 (7)
      • 테스트 (3)
    • 데이터베이스 (3)
      • SQL (7)
      • JPA (1)
      • ElasticSearch (3)
    • CS (3)
    • 배포, 운영 (6)
      • Infra (3)
    • 디자인 패턴 (8)
    • 기타 (6)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • dp
  • 자바 #백준
  • swea #구현
  • 백준 #BFS
  • 알고리즘 #다익스트라
  • 배포
  • 자바 #그리디
  • 코딩테스트
  • 자바
  • 백준 #DP
  • 프로그래머스 #알고리즘
  • Elasticsearch #Testcontainer #Test
  • Elasticsearch #최적화
  • bfs #알고리즘
  • API테스트 #Postman
  • java
  • 백준
  • 알고리즘 #bfs
  • BFS
  • db

최근 댓글

최근 글

hELLO · Designed By 정상우.
Ash_jisu
[Android] 화면 대기(백그라운드) 상태에서 GPS 위치 수집 및 주기적 데이터 전송 구현하기
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.