Skip to content

Background Location Tracking

์ด์ง€๋ฏผ edited this page Dec 15, 2022 · 1 revision

Mission ํ™”๋ฉด์—์„œ User์˜ ์ด๋™ ๊ฒฝ๋กœ ๊ทธ๋ฆฌ๊ธฐ

Plz Stop์˜ Mission์€ ์‚ฌ์šฉ์ž๊ฐ€ ๋ง‰์ฐจ ์‹œ๊ฐ„๋ณด๋‹ค ๋จผ์ € ์ •๋ฅ˜์žฅ์— ๋„์ฐฉํ•˜๋Š”์ง€ ์‹œํ•ฉ์„ ๊ฒจ๋ฃฐ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋Šฅ์ด๋‹ค.

์ด๋•Œ ์‚ฌ์šฉ์ž์˜ ์ด๋™๊ฒฝ๋กœ๋ฅผ ์ง€๋„์— ๊ทธ๋ ค์ฃผ๊ธฐ ์œ„ํ•ด์„œ๋Š” Background์—์„œ์˜ Location Tracking์ด ํ•„์š”ํ•˜๋‹ค.

์ด ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ๊ฒช์—ˆ๋˜ ๊ณผ์ •์€ ์•„๋ž˜์™€ ๊ฐ™๋‹ค.

๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ฒ˜๋ฆฌ

๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ž‘์—…์€ ์ฆ‰์‹œ, ์ง€์—ฐ, ์ •์‹œ๋กœ ๋‚˜๋‰œ๋‹ค.

  • ์ฆ‰์‹œ : ์‚ฌ์šฉ์ž๊ฐ€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๋Š” ๋™์•ˆ ์ž‘์—…์„ ์™„๋ฃŒํ•ด์•ผํ•œ๋‹ค
  • ์ •์‹œ : ์ž‘์—…์„ ์ •ํ™•ํ•œ ์‹œ๊ฐ„์— ์‹คํ–‰ํ•ด์•ผ ํ•œ๋‹ค.
  • ์ง€์—ฐ : ์ž‘์—…์„ ์ •ํ™•ํ•œ ์‹œ๊ฐ„์— ์‹คํ–‰ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.

์‚ฌ์šฉ์ž์˜ ์ด๋™๊ฒฝ๋กœ ๊ทธ๋ฆฌ๊ธฐ๋Š” location์„ ๋ฐ”๋กœ๋ฐ”๋กœ ๋ฐ›์•„์™€ ํ™”๋ฉด์— ๊ทธ๋ ค์ค˜์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์ฆ‰์‹œ ์‹คํ–‰ํ•ด์•ผ ํ•˜๋Š” ์ž‘์—…์ด๋‹ค.

์ฆ‰์‹œ ์‹คํ–‰ํ•ด์•ผ ํ•˜๋Š” ์ž‘์—…์—๋Š” WorkManager์™€ Foreground Service๋ฅผ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

์žฅ์‹œ๊ฐ„ ์‹คํ–‰๋˜๊ณ  ์‚ฌ์šฉ์ž์—๊ฒŒ ์ง„ํ–‰ ์ค‘์ž„์„ ์•Œ๋ ค์•ผํ•˜๋Š” ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•˜๋ ค๋ฉด Foreground Service๋ฅผ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค.

์ฒ˜์Œ์— WorkManager๋ฅผ ์ด์šฉํ•ด Notification์„ ๋„์›Œ์ฃผ์—ˆ๋Š”๋ฐ, ๋ฏธ์…˜์ด ์‹œ์ž‘ํ•˜๊ณ  ๋‚œ ์งํ›„ ๋ฐ”๋กœ ๋œจ์ง€์•Š๊ณ  ์•ฝ๊ฐ„์˜ ๋”œ๋ ˆ์ด๊ฐ€ ์กด์žฌํ•ด Foreground Service๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ํ•˜์˜€๋‹ค.

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 Location

๋ฏธ์…˜ ๊ธฐ๋Šฅ์„ ์ด์šฉํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” 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

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

        }
    }

BroadcastReceiver

์œ„์—์„œ 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)
		}

๋ฐœํ‘œ ์ž๋ฃŒ

๊ธฐ์ˆ  ๊ณต์œ 

๊น€์‹œ์ง„

์ด์ข…์„ฑ

์ด์ง€๋ฏผ

์กฐ๊ฒฝํ˜„

PlzStop

Ground Rule

์ปจ๋ฒค์…˜

์„ค์ •

Daily Scrum

Clone this wiki locally