기능 필요 이유
마라톤 관련 모바일&워치 앱 프로젝트 진행중 스마트폰 GPS를 통해 사용자의 거리 측정 데이터를 저장 및 전송해야 했습니다. 관련해서 워치는 블루트스 데이터 통신이었고 DB는 Firebase Realtime Database를 사용했습니다. 5초마다 GPS거리를 기반으로 누적 뛴 거리를 저장해야하는 로직이 필요했고 이를 위해 백그라운드 상태에서도 GPS위치에 따른 거리 계산 및 데이터 전송이 진행되도록 했습니다.
플로우(검정색 글씨만 이번 글에서 표현)
- 앱 실행-> 포그라운드 서비스 시작(Notification 생성) -> 권한 검사
- 5초마다 GPS 워치 수신 -> 이전 워치와 비교해 거리 계산 - > 누적 거리 업데이트
- Firebase Realtime DB로 누적거리 전송 -> Wear OS 워치로 실시간 거리 전송(이 내용은 타 팀원 작업 예정이라 현재 글에선 제외하겠습니다.)
- 사용자가 "측정 종료" 시 -> 포그라운드 서비스 중지 -> 워치 업데이트 종료
실습 과정
권한, 의존성 추가
권한 추가
<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 |
기능 필요 이유
마라톤 관련 모바일&워치 앱 프로젝트 진행중 스마트폰 GPS를 통해 사용자의 거리 측정 데이터를 저장 및 전송해야 했습니다. 관련해서 워치는 블루트스 데이터 통신이었고 DB는 Firebase Realtime Database를 사용했습니다. 5초마다 GPS거리를 기반으로 누적 뛴 거리를 저장해야하는 로직이 필요했고 이를 위해 백그라운드 상태에서도 GPS위치에 따른 거리 계산 및 데이터 전송이 진행되도록 했습니다.
플로우(검정색 글씨만 이번 글에서 표현)
- 앱 실행-> 포그라운드 서비스 시작(Notification 생성) -> 권한 검사
- 5초마다 GPS 워치 수신 -> 이전 워치와 비교해 거리 계산 - > 누적 거리 업데이트
- Firebase Realtime DB로 누적거리 전송 -> Wear OS 워치로 실시간 거리 전송(이 내용은 타 팀원 작업 예정이라 현재 글에선 제외하겠습니다.)
- 사용자가 "측정 종료" 시 -> 포그라운드 서비스 중지 -> 워치 업데이트 종료
실습 과정
권한, 의존성 추가
권한 추가
<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 |