-
Notifications
You must be signed in to change notification settings - Fork 2
Background Location Tracking

Plz Stop์ Mission์ ์ฌ์ฉ์๊ฐ ๋ง์ฐจ ์๊ฐ๋ณด๋ค ๋จผ์ ์ ๋ฅ์ฅ์ ๋์ฐฉํ๋์ง ์ํฉ์ ๊ฒจ๋ฃฐ ์ ์๋ ๊ธฐ๋ฅ์ด๋ค.
์ด๋ ์ฌ์ฉ์์ ์ด๋๊ฒฝ๋ก๋ฅผ ์ง๋์ ๊ทธ๋ ค์ฃผ๊ธฐ ์ํด์๋ Background์์์ Location Tracking์ด ํ์ํ๋ค.
์ด ๊ธฐ๋ฅ์ ๊ตฌํํ๊ธฐ ์ํด ๊ฒช์๋ ๊ณผ์ ์ ์๋์ ๊ฐ๋ค.
๋ฐฑ๊ทธ๋ผ์ด๋ ์์ ์ ์ฆ์, ์ง์ฐ, ์ ์๋ก ๋๋๋ค.

- ์ฆ์ : ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ๊ณผ ์ํธ์์ฉํ๋ ๋์ ์์ ์ ์๋ฃํด์ผํ๋ค
- ์ ์ : ์์ ์ ์ ํํ ์๊ฐ์ ์คํํด์ผ ํ๋ค.
- ์ง์ฐ : ์์ ์ ์ ํํ ์๊ฐ์ ์คํํ ํ์๊ฐ ์๋ค.
์ฌ์ฉ์์ ์ด๋๊ฒฝ๋ก ๊ทธ๋ฆฌ๊ธฐ๋ location์ ๋ฐ๋ก๋ฐ๋ก ๋ฐ์์ ํ๋ฉด์ ๊ทธ๋ ค์ค์ผํ๊ธฐ ๋๋ฌธ์ ์ฆ์ ์คํํด์ผ ํ๋ ์์ ์ด๋ค.
์ฆ์ ์คํํด์ผ ํ๋ ์์ ์๋ WorkManager์ Foreground Service๋ฅผ ์ด์ฉํ ์ ์๋ค.
์ฅ์๊ฐ ์คํ๋๊ณ ์ฌ์ฉ์์๊ฒ ์งํ ์ค์์ ์๋ ค์ผํ๋ ์์ ์ ์ํํ๋ ค๋ฉด Foreground Service๋ฅผ ์ฌ์ฉํด์ผํ๋ค.
์ฒ์์ WorkManager๋ฅผ ์ด์ฉํด Notification์ ๋์์ฃผ์๋๋ฐ, ๋ฏธ์ ์ด ์์ํ๊ณ ๋ ์งํ ๋ฐ๋ก ๋จ์ง์๊ณ ์ฝ๊ฐ์ ๋๋ ์ด๊ฐ ์กด์ฌํด Foreground Service๋ก ๋ง์ด๊ทธ๋ ์ด์ ํ์๋ค.
Service๋ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ค๋ ์คํ๋๋ ์์ ์ ์ํํ ์ ์๋ ์ ํ๋ฆฌ์ผ์ด์ ๊ตฌ์ฑ์์์ด๋ค.
- Foreground : ์ฌ์ฉ์์๊ฒ ๋ณด์ด๋ ์์ . ์ฑ๊ณผ ์ํธ์์ฉ ํ ์ ์์. ex) ์์ ์ฑ์ ์ฌ์์ฐฝ
- Background : ์ฌ์ฉ์์๊ฒ ์ง์ ๋ณด์ด์ง ์๋ ์์ . ์ฑ์ด ์ด๋ค ์์ ์ ํ๊ณ ์์ง๋ง ์ฌ์ฉ์๊ฐ ์ง๊ด์ ์ผ๋ก ํ์ธ ํ ์ ์์. ex ) ์ฑ์ ๊บผ๋ ์ง์๋๋ ๋ค์ด๋ก๋
- Bound : ํด๋น ์๋น์ค๋ฅผ ๋ฐ์ธ๋ํ ์กํฐ๋นํฐ๋ค์ด ์ ๋ถ ์ข ๋ฃ๋๊ธฐ ์ ๊น์ง ์๋น์ค๊ฐ ์ ์ง๋จ. ์กํฐ๋นํฐ๋ค์ด ๋ชจ๋ ์ข ๋ฃ๋๋ฉด ์์ ์ด ๋๋์ง ์์๋ ์ข ๋ฃ๋จ.
Android O๋ถํฐ Background Service ์คํ์ด ์ ํ๋์๋ค.
Application์ด Background๋ก ์ด๋ํ๊ฒ ๋๋ฉด, ๋ช ๋ถ ์ ๋์ ํ์ฉ์๊ฐ๋ง ์ฃผ๊ณ System์ Background Service๋ฅผ ์ข ๋ฃ์ํจ๋ค.
Background์ผ ๋์ ์ฌ์ฉ์์ ์ด๋๊ฒฝ๋ก๋ฅผ ์ง์ํด์ ๊ทธ๋ ค์ฃผ๊ธฐ ์ํด์ Foreground Service๋ฅผ ๋ง๋ค์ด Location์ ๋ฐ์์ค๋๋ก ๊ตฌํํ๋ค.
Foreground Service๋ฅผ ๊ตฌํํ๊ธฐ ์ํด Notification์ ๋ง๋ค์ด Foreground๋ก ๋ง๋ค์ด์ฃผ์๋ค.

class MissionService : LifecycleService() {
private val notificationManager by lazy {
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}
...
override fun onCreate() {
super.onCreate()
setForeground()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
getTimer(intent)
getStatus(intent)
return super.onStartCommand(intent, flags, startId)
}
private fun setForeground() {
createNotification()
getPersonLocation()
}
private fun createNotification() {
val id = applicationContext.getString(R.string.mission_notification_channel_id)
val title = applicationContext.getString(R.string.mission_notification_title)
createChannel(id)
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
Intent(applicationContext, AlarmActivity::class.java).apply {
putExtra("MISSION_CODE", MISSION_CODE)
flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
},
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val notification = NotificationCompat.Builder(this, id)
.setContentTitle(title)
.setTicker(title)
.setContentText(NOTIFICATION_CONTENT)
.setSmallIcon(R.mipmap.ic_bus)
.setOngoing(true)
.setContentIntent(pendingIntent)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.build()
startForeground(NOTIFICATION_ID, notification)
}
private fun createChannel(id: String) {
if (isMoreThanOreo()) {
if (notificationManager.getNotificationChannel(id) == null) {
val name = this.getString(R.string.mission_notification_channel_name)
NotificationChannel(id, name, NotificationManager.IMPORTANCE_DEFAULT).apply {
notificationManager.createNotificationChannel(this)
}
}
}
}
...
}
๋ฏธ์ ๊ธฐ๋ฅ์ ์ด์ฉํ๊ธฐ ์ํด์๋ Background์ ์์น์ ๋ณด๋ฅผ ์ป์ด์ผํ๋ค.
Background ์์น ์ ๋ณด๋ฅผ ์ป๊ธฐ์ํด์๋ Android 10 ์ด์์์๋ **ACCESS_BACKGROUND_LOCATION**
๊ถํ์ด ํ์ํ๋ค. ์ด์ ๋ฒ์ ์์๋ Foreground ์์น ์ ๋ณด ์ก์ธ์ค ๊ถํ์ ์์ ํ๋ฉด ์๋์ผ๋ก Background ์์น ์ ๋ณด ์ก์ธ์ค ๊ถํ๋ ์์ ํ๋ค.
Plz Stop์ ๊ฒฝ์ฐ ๋ฏธ์ ํ๋ฉด ์ด์ธ์ ํ ํ๋ฉด์์ ์ง๋๋ฅผ ์ฌ์ฉํ๊ณ ์๊ธฐ ๋๋ฌธ์ ์ฑ์ ์คํ์ํค๋ฉด ๋จผ์ Foreground ์์น๋ฅผ ์์ฒญํ๋ค. ์ดํ์ ๋ฏธ์ ํ๋ฉด์ ์ง์ ํ๊ฒ ๋๋ฉด ๊ทธ ๋ Background ์์น๋ฅผ ์์ฒญํ๊ณ ์๋ค.
์ด๋ ๊ฒ ์์น ์ ๋ณด ์ก์ธ์ค ๊ถํ์ ์ ์ง์ ์์ฒญ์ ์คํํ๋ฉด ์ฌ์ฉ์๊ฐ ์ฑ์ ์ด๋ค ๊ธฐ๋ฅ์ ๋ฐฑ๊ทธ๋ผ์ด๋ ์์น ์ ๋ณด ์ก์ธ์ค ๊ถํ์ด ํ์ํ์ง ๋ ์ ํ์ ํ ์ ์์ผ๋ฏ๋ก ์ฌ์ฉ์์๊ฒ ๋ ๋ง์ ์ปจํธ๋กค๊ณผ ํฌ๋ช ์ฑ์ ์ ๊ณตํ ์ ์๋ค.
๋ํ, Andorid 11 ์ด์์์๋ Foreground์ Background ์์น ์ ๋ณด ์์ธ์ค ๊ถํ์ ๋์์ ์์ฒญํ๋ฉด ์์คํ ์ด ์์ฒญ์ ๋ฌด์ํ๊ณ ์ฑ์ ์ด๋ค ๊ถํ๋ ๋ถ์ฌํ์ง ์๊ฒ ๋๋ค.
Android 10์์๋ Background ์์น๋ฅผ ์์ฒญํ๋ฉด ์์คํ ๊ถํ ๋ํ์์์์ ํญ์ ํ์ฉ์ด๋ผ๋ ์ต์ ์ด ํฌํจ๋๋ค.
ํ์ง๋ง Android 11 ์ด์์์๋ ์์คํ ๋ํ์์์ ํญ์ ํ์ฉ ์ต์ ์ด ํฌํจ๋์ง ์๋๋ค. ์ค์ ํ์ด์ง์์ Background ์์น๋ฅผ ์ฌ์ฉ ์ค์ ํด์ผํ๋ค.
๋ฐ๋ผ์ Dialog๋ฅผ ๋์์ ์ค์ ํ์ด์ง๋ก ์ด๋ํด ์ฌ์ฉ์๊ฐ ํญ์ ํ์ฉ ๊ถํ์ ์ ํํ๊ฒ ํ์๋ค.


FusedLocationProvider๋ ๊ตฌ๊ธ์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ด๋ฉฐ ๊ธฐ๊ธฐ์ ๋ฐฐํฐ๋ฆฌ ์ ๋ ฅ ์ฌ์ฉ์ ์ต์ ํํด์ค๋ค.
LocationCallback
์ ์ด์ฉํด ์ด๋ ์์น๋ฅผ ๋ฐ์์ค๊ณ ์ด๋ฅผ ArrayList์ ์ ์ฅํด ์ฌ์ฉ์์ ์ด๋๊ฒฝ๋ก๋ฅผ ๊ทธ๋ฆด ๋ ์ฌ์ฉํ์๋ค.
Fragment๊ฐ ์ฃฝ์ด ๋ค์ ์์ฑ๋๋ฉด ์ด์ ๊ฒฝ๋ก๋ค์ Service์์ ๋ฐ์์ ๊ทธ๋ ค์ฃผ๊ฒ ๋๋ค.
// MissionService.kt
private fun getPersonLocation() {
if (ActivityCompat.checkSelfPermission(
applicationContext,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
applicationContext,
Manifest.permission.ACCESS_COARSE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) {
return
}
fusedLocationClient.lastLocation
.addOnSuccessListener { location ->
if (location != null) {
userLocation.add(Location(location.latitude, location.longitude))
}
}
.addOnFailureListener {
it.printStackTrace()
}
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult) {
for (location in locationResult.locations) {
if (location != null) {
userLocation.add(Location(location.latitude, location.longitude))
}
}
}
}
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, Looper.getMainLooper())
}
// MissionFragment.kt
private fun drawPersonLine() {
lateinit var beforeLocation: Location
viewLifecycleOwner.lifecycleScope.launch {
missionViewModel.userLocations.collectIndexed { index, userLocation ->
if (index == 1) {
initMarker(userLocation) // <- ์ด ํจ์์์ ์ด์ ๊ฒฝ๋ก๋ฅผ ๊ทธ๋ ค์ฃผ๊ฒ ๋๋ค.
beforeLocation = userLocation.last()
} else if (index > 1) {
drawNowLocationLine(
TMapPoint(userLocation.last().latitude, userLocation.last().longitude),
TMapPoint(beforeLocation.latitude, beforeLocation.longitude)
)
personCurrentLocation = userLocation.last()
if (tMap.isTracking) {
tMap.tMapView.setCenterPoint(userLocation.last().latitude, userLocation.last().longitude)
}
beforeLocation = userLocation.last()
arriveDestination(userLocation.last().latitude, userLocation.last().longitude)
}
}
}
}
private fun initMarker(nowLocation: ArrayList<Location>) {
with(tMap) {
addMarker(
Marker.PERSON_MARKER,
Marker.PERSON_MARKER_IMG,
TMapPoint(nowLocation.last().latitude, nowLocation.last().longitude)
)
personCurrentLocation = nowLocation.last()
latitudes.add(nowLocation.last().latitude)
longitudes.add(nowLocation.last().longitude)
setRouteDetailFocus()
arriveDestination(nowLocation.last().latitude, nowLocation.last().longitude)
// ์ด์ ๊ฒฝ๋ก๋ฅผ ๊ทธ๋ ค์ฃผ๋ ํจ์
drawWalkLines(
nowLocation.map { TMapPoint(it.latitude, it.longitude) } as ArrayList<TMapPoint>,
Marker.PERSON_LINE + PERSON_LINE_NUM.toString(),
Marker.PERSON_LINE_COLOR
)
PERSON_LINE_NUM += 1
}
}
์์์ FusedLocationProvider๋ก ์ป์ Location์ Broadcast๋ฅผ ํตํด Fragment์ ์ ๋ฌํ์๋ค.
// MissionService.kt
private fun sendUserInfo() {
val statusIntent = Intent().apply {
action = MISSION_USER_INFO
putExtra(MISSION_LAST_TIME, lastTime)
putParcelableArrayListExtra(MISSION_LOCATIONS, userLocation)
}
sendBroadcast(statusIntent)
sendMissionStatus()
}
private fun sendMissionStatus() {
val statusIntent = Intent().apply {
action = MISSION_STATUS
putExtra(MISSION_STATUS, true)
}
sendBroadcast(statusIntent)
}
// MissionFragment.kt
private fun setBroadcastReceiver() {
val intentFilter = IntentFilter().apply {
addAction(MissionService.MISSION_USER_INFO)
}
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
missionViewModel.lastTime.value = intent?.getStringExtra(MISSION_LAST_TIME)
missionViewModel.userLocations.value =
intent?.getParcelableArrayListExtra<Location>(MISSION_LOCATIONS) as ArrayList<Location>
}
}
requireActivity().registerReceiver(receiver, intentFilter)
}
๊น์์ง
- Plz Stop์ ๊ตฌํํ๋ฉด์ ๊ณต๋ถํ ๋ด์ฉ
- ์๋ ๊ธฐ๋ฅ์ fancyํ๊ฒ ๊ตฌํํ๊ธฐ๊น์ง์ story
- Repository ํจํด์ด ์๊ณ ์ถ๋ค
- DataStore ๋ ํน์ ๋ญ ๋ผ?
- Flow SingleEvent
- Flow Debounce๋ก API ์์ฒญ์ ์ค์ฌ๋ณด์
- WorkManager ๋ฟ์ ๋ฟ์
์ด์ข ์ฑ
- โณ ์ค์ง ์๋ ๋ง์ฐจ ์๊ฐ์ ์ฐพ์์
- ๐ NavGraphViewModel์ ์ฌ์ฉํ๊ฒ ๋ ์ด์
- โค ๊ณต์ ๋ฌธ์ ๋ง์ ์ ๋ค์ด์ผ ํ๋ ์ด์ with setGraph
์ด์ง๋ฏผ
์กฐ๊ฒฝํ