ForegroundService를 위한 WorkManager 사용

정용우·2023년 8월 31일

문제점


산책하는 거리를 계산하기 위해 일정 시간마다 해당 위치를 저장하고 각 위치간의 거리와 산책 소요 시간을 포그라운드 서비스를 통해 출력하고자 하였으나...

포그라운드 서비스를 사용하기 위해 기존 코드를 사용하려 했으나 android 12부터 몇 가지 특수한 사례를 제외하고 포그라운드 서비스를 사용할 수 없게 되었다. 공식 페이지에서는 대안으로 WorkManager를 추천하여 사용하게 되었다.

WorkManager란


지속적인 작업에 권장되는 방식. 앱이 다시 시작되거나 시스템이 재부팅되어도 작업이 예약된 채로 남아 있고 작업이 유지됨. 대부분의 백그라운드 처리는 지속적인 작업에 처리되므로 백그라운드 처리에 권장되는 api임

android 12부터는 WorkManager 없이 앱이 백그라운드에서 실행되는 동안 포그라운드 서비스를 시작하려고 해도 포그라운드 서비스가 예외 사례 중 하나를 충족하지 못하면 시스템에서  ForegroundServiceStartNotAllowedException이 발생.

해결


CoroutineWorker를 상속받은 WorkManager를 생성하고 내부에서 LocationManager를 통해 감지된 location을 통해 이동 거리를 계산한다

override suspend fun doWork(): Result {
    Log.d(TAG, "doWork: 산책 시작")

    val foregroundInfo = createForegroundInfo(notificationContent)
    setForegroundAsync(foregroundInfo)

    handler.post {
        getProviders()
    }

    simulateLocationUpdates()

    return Result.success()
}

ForegroundInfo를 생성하여 setForegroundAsync에 등록한다. 이후 getProviders를 통해 지속적으로 location값을 설정하고 simulateLocationUpdates를 통해 설정된 location간의 거리 값을 계산하여 ForegroundService를 통해 출력한다
private fun createForegroundInfo(progress: String): ForegroundInfo {
    createNotificationChannel()

    val title = applicationContext.getString(R.string.app_name)

    val notification = NotificationCompat.Builder(applicationContext, FOREGROUND_SERVICE_ID.toString())
        .setContentTitle("지금은 산책중입니다~")
        .setTicker(title)
        .setContentText(progress)
        .setSmallIcon(R.drawable.main_logo_small)
        .setOngoing(true)
        .build()

    return ForegroundInfo(FOREGROUND_SERVICE_ID, notification)
}

private fun createNotificationChannel() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val manager: NotificationManager =
            applicationContext.getSystemService(NotificationManager::class.java)
        val serviceChannel = NotificationChannel(
            FOREGROUND_SERVICE_ID.toString(),
            applicationContext.getString(R.string.app_name),
            NotificationManager.IMPORTANCE_LOW,
        )
        manager.createNotificationChannel(serviceChannel)
    }
}

ForegroundService로 띄울 Notification에 대한 채널 생성과 데이터를 설정한다.
private val locationManager by lazy {
    applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
}
private lateinit var location1: Location
private lateinit var location2: Location

private fun getProviders() {
    locationManager.requestLocationUpdates(
        LocationManager.GPS_PROVIDER,
        0,
        0f,
        listener,
    )
}

private val listener = object : LocationListener {
    // 위치가 변경될때 호출될 method
    override fun onLocationChanged(location: Location) {
        when (location.provider) {
            LocationManager.GPS_PROVIDER -> {
                location2 = location
            }
        }
    }

    override fun onProviderEnabled(provider: String) {
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, this)
    }
}

LocationManager를 선언하여 gps값을 받아 저장한다. 산책은 실외에서 하는 점, 저장된 location간에 거리 값을 비교해야 하는 점에서 GPS_PROVIDER만 사용하여 저장한다. 권한에 대한 설정은 WorkManager 코드 외부에서 처리하였다.
private var totalDist = 0F
private var time = 0

private suspend fun simulateLocationUpdates() {
    while (true) {
        delay(1000) // Simulate delay between updates
        if (this::location1.isInitialized) {
            totalDist += location1.distanceTo(location2)
            WalkFragment.walkDist = totalDist

            Log.d(TAG, "simulateLocationUpdates: 이동거리 $totalDist")
        }
        if (this::location2.isInitialized) {
            location1 = location2
        }
        time++
        WalkFragment.walkTime = time
        updateNotification("산책 거리 : ${StringFormatUtil.distanceIntToString(totalDist.toInt())} \n산책 시간 : ${StringFormatUtil.timeIntToString(time)}")
    }
}

private fun updateNotification(content: String) {
    val foregroundInfo = createForegroundInfo(content)
    setForegroundAsync(foregroundInfo)
}

저장해놓은 location을 1초마다 비교하여 거리 값을 계산하며 updateNotification을 통해 notification의 산책 거리와 시간을 갱신한다. 사용자가 버튼 클릭을 통해 산책을 종료하므로 계속해서 반복하게 코드를 구성하고 WalkFragment에서 버튼 클릭을 통해 `workManager.cancelAllWork()` 로 종료한다. 해당 코드는 WorkManager에 대해 더 공부하고 수정할 필요가 있어 보인다.

전체 코드
class WalkWorker(context: Context, parameters: WorkerParameters) :
    CoroutineWorker(context, parameters) {

    private val locationManager by lazy {
        // ~~Manager는 getSystemService로 호출
        applicationContext.getSystemService(Context.LOCATION_SERVICE) as LocationManager
    }
    private lateinit var location1: Location
    private lateinit var location2: Location

    private var totalDist = 0F
    private var time = 0

    private val handler = Handler(Looper.getMainLooper())

    private val notificationContent = "산책 시작!"

    override suspend fun doWork(): Result {
        Log.d(TAG, "doWork: 산책 시작")

        val foregroundInfo = createForegroundInfo(notificationContent)
        setForegroundAsync(foregroundInfo)
        Log.d(TAG, "doWork: notification 생성")

        handler.post {
            getProviders()
        }
        // Simulate receiving location updates
        simulateLocationUpdates()

        return Result.success()
    }

    // Creates an instance of ForegroundInfo which can be used to update the
    // ongoing notification.
    private fun createForegroundInfo(progress: String): ForegroundInfo {
        createNotificationChannel()

        val title = applicationContext.getString(R.string.app_name)

        val notification = NotificationCompat.Builder(applicationContext, FOREGROUND_SERVICE_ID.toString())
            .setContentTitle("지금은 산책중입니다~")
            .setTicker(title)
            .setContentText(progress)
            .setSmallIcon(R.drawable.main_logo_small)
            .setOngoing(true)
            .build()

        return ForegroundInfo(FOREGROUND_SERVICE_ID, notification)
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val manager: NotificationManager =
                applicationContext.getSystemService(NotificationManager::class.java)
            val serviceChannel = NotificationChannel(
                FOREGROUND_SERVICE_ID.toString(),
                applicationContext.getString(R.string.app_name),
                NotificationManager.IMPORTANCE_LOW,
            )
            manager.createNotificationChannel(serviceChannel)
        }
    }

    private suspend fun simulateLocationUpdates() {
        while (true) {
            delay(1000) // Simulate delay between updates
            if (this::location1.isInitialized) {
                totalDist += location1.distanceTo(location2)
                WalkFragment.walkDist = totalDist

                Log.d(TAG, "simulateLocationUpdates: 이동거리 $totalDist")
            }
            if (this::location2.isInitialized) {
                location1 = location2
            }
            time++
            WalkFragment.walkTime = time
            updateNotification("산책 거리 : ${StringFormatUtil.distanceIntToString(totalDist.toInt())} \n산책 시간 : ${StringFormatUtil.timeIntToString(time)}")
        }
    }

    private fun updateNotification(content: String) {
        val foregroundInfo = createForegroundInfo(content)
        setForegroundAsync(foregroundInfo)
    }

    companion object {
        const val FOREGROUND_SERVICE_ID = 12345
    }

    @SuppressLint("MissingPermission")
    private fun getProviders() {
        locationManager.requestLocationUpdates(
            LocationManager.GPS_PROVIDER,
            0,
            0f,
            listener,
        )
    }

    private val listener = object : LocationListener {
        // 위치가 변경될때 호출될 method
        override fun onLocationChanged(location: Location) {
            when (location.provider) {
                LocationManager.GPS_PROVIDER -> {
                    location2 = location
                }
            }
        }

        @SuppressLint("MissingPermission")
        override fun onProviderEnabled(provider: String) {
            locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0f, this)
        }
    }
}

0개의 댓글