Today's

길을 나서지 않으면 그 길에서 만날 수 있는 사람을 만날 수 없다

모바일 앱(안드로이드)

안드로이드 앱 만들기 : Alarm manager vs Job Scheduler vs Worker

Billcorea 2023. 1. 8. 15:15
반응형

 

손에 들고 다니는 스마트폰에 무슨 일을 그렇게 시켜 먹을라고(?) 이런 것들이 있는 가? 하는 생각이 들 무렵입니다. 

그래도 우린 이제 이런 배치(반복작업을 위한) 처리를 해야 하는 경우가 있어서 이런 것들에 대해서 알아 보고자 합니다. 

 

  • Alarm manager
  • Job Scheduler
  • Worker

반복적인 일을 시키는 방법 3가지를 살펴 보고자 합니다.

Alarm manager

알림은 지정한 시간에 어떤 이벤트가 있는 지를 알려주는 역할을 하게 됩니다.  그것들 중에서 1회성 알림만을 사용하는 경우도 있기는 하겠지만,  앱을 개발하는 경우에는 반복적인 알림을 발생시키는 것이 좋을 때가 있습니다.  그때 사용을 하게 될 것 같습니다. 

 

알림 매니저가 좋은 건 15분 미만의 경우도 반복 작업을 할 수 있다는 점 입니다.

https://developer.android.com/reference/android/app/AlarmManager

 

AlarmManager  |  Android Developers

 

developer.android.com

 

Job Scheduler

 

이름 그대로 일을 스케줄에 맞게 반복적인 작업을 실행하는 경우에 사용하게 될 것 같습니다. 다만,  안드로이드 버전이 높아지면서 배터리 효율을 관리하기 위해 최소 시간이 15분이라는 간격을 유지해야 한다는 불편함(?)이 있다는 것이 아쉽게 다가올 뿐입니다. 

 

https://developer.android.com/reference/android/app/job/JobScheduler

 

JobScheduler  |  Android Developers

 

developer.android.com

 

Worker

비동기식 작업을 설정하는 방법 중에 하나라고는 들었으나, 아직 아는 바가 없어 이 부분에 대한 기술은 훗날로 미루어 봅니다. ㅋ~

https://developer.android.com/reference/androidx/work/Worker

 

Worker  |  Android Developers

androidx.constraintlayout.core.motion.parse

developer.android.com

 

구현해 보기

이제 코드로 하나씩 만들어 보겠습니다.  

 

JobScheduler 관리를 위해서 다음과 같이 선언해 둡니다. jobId 가 있어야 하기 때문이기도 하고, 반복 간격을 지정하기 위한 상수도 선언합니다. 그리고 성공 여부를 확인 하기 위한 상수도 같이 선언 합니다.

private val JOB_ID = 123
private var PERIODIC_TIME: Long = 15 * 60 * 1000
private val SUCCESS_KEY = "SUCCESS"
private val FAILED_KEY = "FAILED"

 

알림 설정을 위해서는 다음과 같이 알림 매니저와 알림 실행 시 사용한 intent 변수 하나를 설정해 둡니다.

private var alarmMgr: AlarmManager? = null
private lateinit var alarmIntent: PendingIntent

 

JobScheduler의 시작은 다음과 같이 코드 작업을 합니다.  jobServices는 반복 작업에서 실행시킬 class 이름입니다. 

  • setPersisted : 부팅된 이후에도 반복을 하게 할지 여부를 선택합니다.
  • setPeriodic : 반복 시간을 mS 단위로 설정하게 됩니다.

다른 선택 값들도 있으니 참고해서 살펴보세요.

val componentName = ComponentName(this, jobServices::class.java)
val info = JobInfo.Builder(JOB_ID, componentName)
    .setPersisted(true)             // true 부팅된 이후에도 반복하게
    .setPeriodic(PERIODIC_TIME) // 지연시간
    .build()

val jobScheduler: JobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
val resultCode = jobScheduler.schedule(info)

val isJobScheduledSuccess = resultCode == JobScheduler.RESULT_SUCCESS
Log.e(TAG, "Job Scheduled ${if (isJobScheduledSuccess) SUCCESS_KEY else FAILED_KEY}")

 

알림의 경우도 알림 상수로 선언한 알림 매니저에 알림 실행 시 사용할 AlarmReceiver을 선언하고  알림 설정을 시작합니다.  반복 작업을 할 예정이기 때문에 Repeting 함수들 중에서 골랐습니다.  반복이 아닌 1회성의 경우는 다른 함수를 사용하게 됩니다. 

  • setInexactRepeating : 알림 시간 이후 반복 시간 동안 절전모드를 해제하여 알림을 발생시킵니다.
  • set : 시정된 시간에 알림을 1회성으로 절전모드를 해제하여 알림을 발생시킵니다.
alarmMgr = getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmIntent = Intent(applicationContext, AlarmReceiver::class.java).let { intent ->
    PendingIntent.getBroadcast(applicationContext, 0, intent, FLAG_MUTABLE)
}

alarmMgr?.setInexactRepeating(
    AlarmManager.ELAPSED_REALTIME_WAKEUP,
    SystemClock.elapsedRealtime() + sp.getFloat("repeatTerm", 1f).toLong() * 60 * 1000, // 1분 단위로
    sp.getFloat("repeatTerm", 1f).toLong() * 60 * 1000,
    alarmIntent
)

 

이제 코드 작업을 시작하였으니 작업 해제 하는 코드를 살펴보겠습니다. 

 

JobScheduler의 경우는 앞에서 시작할 때 지정했던 jobID을 이용하여 반복 작업을 하는 하는 반면에...

val jobScheduler: JobScheduler = getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
jobScheduler.cancel(JOB_ID)
Log.e(TAG, "Job CANCELED")

 

알림의 경우는 알림 매니저를 그냥 cancel 하는 것으로 작업이 종료됩니다.

alarmMgr?.cancel(alarmIntent)

 

이제 각 배치 작업에서 사용할 Service의 코드를 보겠습니다. 

먼저 manifest.xml 파일에 등록한 부분을 보겠습니다. 

<receiver android:name=".receivers.AlarmReceiver"
    android:exported="false"/>

<service
    android:name=".services.jobServices"
    android:exported="true"
    android:permission="android.permission.BIND_JOB_SERVICE" />

알림을 위해서는 receiver로 등록을 하게 되고, jobscheduler을 위해서는 jobservices로 등록을 하게 됩니다.  각 class의 전체 코드는 다음과 같습니다. 

 

JobServices 

import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.job.JobParameters
import android.app.job.JobService
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.billcoreatech.smsreceiver1113.BuildConfig
import com.billcoreatech.smsreceiver1113.MainActivity
import com.billcoreatech.smsreceiver1113.R
import com.billcoreatech.smsreceiver1113.retrofit.RetrofitService
import java.text.SimpleDateFormat
import java.util.*

@SuppressLint("SpecifyJobSchedulerIdRange")
class jobServices : JobService() {

    companion object {
        private val TAG = "MyJobService"
        lateinit var sp: SharedPreferences
    }

    @RequiresApi(Build.VERSION_CODES.S)
    override
    fun onStartJob(params: JobParameters): Boolean {
        var strDate = System.currentTimeMillis()
        var sdf = SimpleDateFormat("yyyy-MM-dd kk:mm:ss", Locale("ko", "KR"))
        var now = sdf.format(strDate )
        var context = this@jobServices

        sp = getSharedPreferences(packageName, MODE_PRIVATE)

        Log.e(TAG, "${now} onStartJob: ${params.jobId} ${sp.getFloat("repeatTerm", 1f).toLong()} ${sp.getBoolean("jobScheduled", false)}")
        sendNotification(this@jobServices, "Activated ...")
        return false
    }

    override
    fun onStopJob(params: JobParameters): Boolean {
        Log.e(TAG, "onStopJob: ${params.jobId}")
        return false
    }

    @RequiresApi(Build.VERSION_CODES.S)
    @SuppressLint("MissingPermission")
    private fun sendNotification(context : Context, messageBody: String) {
        val intent = Intent(context, MainActivity::class.java)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
        val extras = Bundle()
        extras.putString("MSGRCV", messageBody)
        intent.putExtras(extras)
        val pendingIntent = PendingIntent.getActivity(
            context, 0 /* Request code */, intent,
            PendingIntent.FLAG_MUTABLE
        )

        val channelId: String = context.getString(R.string.default_notification_channel_id)
        val channelName: CharSequence = context.getString(R.string.default_notification_channel_name)
        val importance = NotificationManager.IMPORTANCE_HIGH
        val notificationChannel = NotificationChannel(channelId, channelName, importance)
        notificationChannel.enableLights(true)
        notificationChannel.lightColor = Color.RED
        notificationChannel.enableVibration(true)
        notificationChannel.vibrationPattern =
            longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400)

        val wearNotifyManager = NotificationManagerCompat.from(context)
        val wearNotifyBuilder: NotificationCompat.Builder =
            NotificationCompat.Builder(context, channelId)
                .setSmallIcon(R.drawable.ic_outline_sms_24)
                .setContentTitle(context.getString(R.string.app_name))
                .setContentText(messageBody)
                .setAutoCancel(true)
                .setContentIntent(pendingIntent)
                .setVibrate(longArrayOf(100, 200, 300, 400, 500, 400, 300, 200, 400))
                .setDefaults(-1)

        wearNotifyManager.createNotificationChannel(notificationChannel)
        wearNotifyManager.notify(0, wearNotifyBuilder.build())
    }
}

 

AlarmReceiver


import android.app.AlarmManager
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.SystemClock
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import com.billcoreatech.smsreceiver1113.MainActivity
import com.billcoreatech.smsreceiver1113.R

class AlarmReceiver : BroadcastReceiver() {

    companion object {
        const val TAG = "AlarmReceiver"
        const val NOTIFICATION_ID = 0
        const val PRIMARY_CHANNEL_ID = "primary_notification_channel"
    }

    lateinit var notificationManager: NotificationManager

    @RequiresApi(Build.VERSION_CODES.S)
    override fun onReceive(context: Context, intent: Intent) {
        Log.d(TAG, "Received intent : $intent")
        notificationManager = context.getSystemService(
            Context.NOTIFICATION_SERVICE) as NotificationManager

        createNotificationChannel(context)
        deliverNotification(context)

    }

    @RequiresApi(Build.VERSION_CODES.S)
    private fun deliverNotification(context: Context) {
        val contentIntent = Intent(context, MainActivity::class.java)
        val contentPendingIntent = PendingIntent.getActivity(
            context,
            NOTIFICATION_ID,
            contentIntent,
            PendingIntent.FLAG_MUTABLE
        )
        var sp = context.getSharedPreferences(context.packageName, Context.MODE_PRIVATE)

        val builder =
            NotificationCompat.Builder(context, PRIMARY_CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_outline_sms_24)
                .setContentTitle(context.getString(R.string.app_name))
                .setContentText("This is repeating alarm repeatTerm ${sp.getFloat("repeatTerm", 1f).toLong()} min")
                .setContentIntent(contentPendingIntent)
                .setPriority(NotificationCompat.PRIORITY_HIGH)
                .setAutoCancel(true)
                .setDefaults(NotificationCompat.DEFAULT_ALL)

        Log.e("", "This is repeating alarm repeatTerm ${sp.getFloat("repeatTerm", 1f).toLong()} min")

        notificationManager.notify(NOTIFICATION_ID, builder.build())
    }

    private fun createNotificationChannel(context: Context) {

        val notificationChannel = NotificationChannel(
            PRIMARY_CHANNEL_ID,
            context.getString(R.string.app_name),
            NotificationManager.IMPORTANCE_HIGH
        )
        notificationChannel.enableLights(true)
        notificationChannel.lightColor = R.color.softRed
        notificationChannel.enableVibration(true)
        notificationChannel.description = context.getString(R.string.app_name)
        notificationManager.createNotificationChannel(
            notificationChannel)

    }
}

 

두 가지 케이스 모두 그냥 job에서는 Notification을 발생시키는 것이 목적인 코드입니다.  이제 코드작성 방법은 다 모았으니 다른 작업으로 발전해 나갈 수 있겠지요?

 

 

반응형