[Android Compose] Foreground Service로 백그라운드에서 위치 정보 가져오기

: ) YOUNG·2024년 7월 15일
1

안드로이드

목록 보기
25/30
post-thumbnail
post-custom-banner

Compose Foreground Service로 백그라운드에서 위치 정보 가져오기



먼저 권한 설정을 위해서 Manifest에 설정을 해줍니다.

ACCESS_BACKGROUND_LOCATION 권한은 안드로이드 10(Q) 버전부터 도입되었습니다.
이 권한은 앱이 백그라운드에서 사용자의 위치 정보에 접근할 수 있도록 합니다.

<service>는 앱이 foreground에서 동작할 수 있도록 하는 설정입니다.
본인이 만들어둔 Service.kt 파일 class를 넣어주는 것입니다.

저의 경우에는 Service()를 받은 class가 RecordingService이므로 해당 이름으로 설정해두었습니다.
// ex .service.RecordingService
android:foregroundServiceType="location" 은 위치 정보를 foreground에서 받아오기 위해 넣어주었습니다.

Manifest.XML


    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
	<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

    <application>
    <activity>
      ...

    </activity>
            <service
                android:name="{본인의 Service class 파일 경로}" 
                android:enabled="true"
                android:exported="true"
                android:foregroundServiceType="location" />

    </application>
  

다음은 Service 구현체입니다.
Service에서 Foreground를 실행시키는 구현체가 만들어지고, 앱이 백그라운드 상태에서도 지속적인 위치 정보를 가져오도록 합니다.

정확히 위치 정보가 나오게 되는 건 onCreate()에서 구현한 LocationCallbackonLocationResult()에서 정보를 수집할 수 있습니다.

위치 데이터를 Screen에 전달할 때는 Broadcast Receiver, ViewModel, EventBus 등 다양한 방법으로 각자 용도에 맞도록 전송하시면 될 것 같습니다.

val locationInterval = 5000L이 시간을 설정하여 얼마의 시간마다 새로운 위치 정보를 가져올지 설정할 수 있습니다.
1000L이 1초이니 여기서는 5초가 됩니다.

또한 Priority.PRIORITY_HIGH_ACCURACY 옵션은 위치의 정확도와 배터리 효율성을 기준으로 하는 옵션이니,
아래 공식 문서를 읽어보시고 본인의 용도에 맞도록 잘 설정해서 사용하는 것을 추천드립니다.



Service 구현


Service.kt


class RecordingService : Service() { // End of RunningService class

    private lateinit var fusedLocationProviderClient: FusedLocationProviderClient
    private lateinit var locationCallback: LocationCallback

    // Context
    private lateinit var context: Context
    
    override fun onCreate() {
        super.onCreate()
        context = applicationContext
        fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
    } // End of onCreate()


    override fun onBind(p0: Intent?): IBinder? {
        return null
    } // End of onBind()

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            Actions.START.toString() -> start()
            Actions.STOP.toString() -> stopSelf()
        }
        return super.onStartCommand(intent, flags, startId)
    } // End of onStartCommand()


    private fun start() {
        val intent = Intent(this, MainActivity::class.java).apply {
            addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
        }
        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent, PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )
        
        
        val locationInterval = 5000L
        val locationRequest =
            LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, locationInterval)
                .setWaitForAccurateLocation(false).build()

        locationCallback = object : LocationCallback() {
            override fun onLocationResult(locationResult: LocationResult) {
                super.onLocationResult(locationResult)
                val location = locationResult.locations[0]

                Log.d(TAG, "onLocationResult: $location")
            }
        }


        fusedLocationProviderClient.requestLocationUpdates(
            locationRequest, locationCallback, Looper.getMainLooper()
        )
        
 
        val notification = NotificationCompat.Builder(this, "foreground_channel")
            .setContentTitle("Title")
            .setContentIntent(pendingIntent).setContentText("Elapsed time").build()
        startForeground(1, notification)
    } // End of start()


    override fun onDestroy() {
        stopForeground(true)
        stopSelf()
        super.onDestroy()
    }


    enum class Actions {
        START, STOP
    } // End of Actions class
} // End of RunningService class


NotificationManager 설정

다음 Application에서 NotificationManager를 설정합니다.

Application.kt

...

    override fun onCreate() {
        super.onCreate()


        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager =
                getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

            val channel = NotificationChannel(
                "walking_channel", "walking_channel",
                NotificationManager.IMPORTANCE_HIGH
            )

            notificationManager.createNotificationChannel(channel)
        }
    } // End of onCreate()

...


생명주기에 따른 실행과 종료 시키기

이제 Foreground Service를 실행할 수 있습니다!

저 같은 경우는 특정 화면에 들어서거나, 버튼으로 동작하도록 했는데, 여기서는 가장 쉬운 예시를 들기 위해 생명주기에 따라 실행되도록 했습니다.
특정 Screen이 실행되었을 때 자동으로 Foregroud ServiceSTART 되고, Screen이 종료되었을 때 Foregroud ServiceSTOP 되도록 구현하였습니다.

이것은 예시이니 본인이 설정하고 싶은 타이밍에 맞도록 START를 넣어주시면 될 것 같습니다.


	val lifeCycleOwner = LocalLifecycleOwner.current

    DisposableEffect(lifeCycleOwner) {
        val observer = LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_START) {
                // onStart 에 자동으로 Service를 시작하고 싶다면 여기 넣기
                Intent(
                    context.applicationContext,
                    RecordingService::class.java,
                ).also {
                    it.action = RecordingService.Actions.START.toString()
                    context.applicationContext.startService(it)
                }
                
            } else if (event == Lifecycle.Event.ON_STOP) {
                // ON_STOP 이 되면 service 종료
                Intent(
                    context.applicationContext,
                    RecordingService::class.java,
                ).also {
                    it.action = RecordingService.Actions.STOP.toString()
                    context.applicationContext.startService(it)
                }
            }
        }

        lifeCycleOwner.lifecycle.addObserver(observer)

        return@DisposableEffect onDispose {
            lifeCycleOwner.lifecycle.removeObserver(observer)
        }
    }



궁금하신 부분은 댓글로 남겨주시면 언제든지 답변해드립니다!


post-custom-banner

0개의 댓글