I am trying to schedule a notification that needs to be displayed every hour, sometimes 45 minutes, depends on various factors unrelated to this question. For testing purposes, I am running it every 10 minutes with a maximum execution latency of 1 minute. My goal is to have a service that plans notifications, and then restarts itself after 10 minutes with a maximum execution length of 1 minute. In the future, the planning service needs to retrieve something from the internet, so the JobInfo NetworkType should be set to Any. The problem with my current code, however, is that it does reschedule it every 10 minutes, but not always. It sometimes randomly doesn't show a notification for 3 hours (using the phone, internet active) and then start showing notifications again. What am I doing wrong causing it to sometimes skip firing for several hours.
Note: I would not like to use the AlarmManager, as it does not persist jobs through reboots and I'd rather use JobScheduler for the job.
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Plan the notification plan service
scheduleJob(
context = this,
time = Calendar.getInstance(Util.TIMEZONE).timeInMillis,
executionTime = TimeUnit.MINUTES.toMillis(1),
service = PlanNotificationsService::class.java,
extras = PersistableBundle()
)
}
}
PlanNotificationsService.kt
class PlanNotificationsService : JobService() {
override fun onStopJob(params: JobParameters?): Boolean {
log("Service Abruptly stopped")
return true
}
override fun onStartJob(job: JobParameters?): Boolean {
log("Plan Notification On Start Job")
GlobalScope.async {
val now = Calendar.getInstance(Util.TIMEZONE)
log("Scheduling for " + (now.timeInMillis + TimeUnit.MINUTES.toMillis(1)))
log("Scheduling for " +
"${now.get(Calendar.HOUR_OF_DAY)}:" +
"${now.get(Calendar.MINUTE) + 10}:" +
"${now.get(Calendar.SECOND)}")
// Schedule planning of notifications (itself)
Util.scheduleJob(
context = applicationContext,
time = now.timeInMillis + TimeUnit.MINUTES.toMillis(10),
executionTime = TimeUnit.MINUTES.toMillis(1),
service = PlanNotificationsService::class.java,
extras = PersistableBundle(),
persist = true
)
// Schedule notification
Util.scheduleJob(
context = applicationContext,
time = now.timeInMillis,
executionTime = TimeUnit.MINUTES.toMillis(1),
service = NotificationJobService::class.java,
extras = PersistableBundle().apply {
putString("Title", "Test Title")
putString("Message", "Test message")
}
)
jobFinished(job, false)
}
return true
}
}
NotificationJobService
class NotificationJobService : JobService() {
override fun onStopJob(job: JobParameters?): Boolean = false
override fun onStartJob(job: JobParameters?): Boolean {
log("Notification On Start Job")
job?.extras?.let { extras ->
GlobalScope.async {
scheduleNotification(extras)
jobFinished(job, false)
}
}
return true
}
private fun scheduleNotification(bundle: PersistableBundle) {
log("Notification Creating Notification")
try {
val notificationManager = applicationContext
.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val title = bundle.getString("Title")
val message = bundle.getString("Message")
val icon = R.drawable.ic_launcher_background
// TODO COLOR & DETAILS INTENT
val builder = NotificationCompat.Builder(applicationContext)
.apply {
setSmallIcon(icon)
setContentTitle(title)
setContentText(message)
setStyle(NotificationCompat.BigTextStyle().bigText(message))
setAutoCancel(true)
setDefaults(0)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel(applicationContext)
builder.setChannelId(SCHEDULE_NOTIFICATION_ID)
}
notificationManager.notify((Math.random() * Int.MAX_VALUE).toInt(), builder.build())
} catch (e: Exception) {
log("Fucked Up")
e.printStackTrace()
}
}
private fun createNotificationChannel(context: Context) {
log("Notification Creating Notification Channel")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val notificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
if (notificationManager.getNotificationChannel(SCHEDULE_NOTIFICATION_ID) != null) return
val notificationChannel = NotificationChannel(SCHEDULE_NOTIFICATION_ID, "Schedule Notifications", NotificationManager.IMPORTANCE_LOW)
notificationChannel.enableLights(true)
//TODO COLOR
notificationManager.createNotificationChannel(notificationChannel)
}
}
}
Util.kt
class Util {
companion object {
val TAG = "BackNotiTag"
val SCHEDULE_NOTIFICATION_ID = "BackgroundNotification"
val TIMEZONE = TimeZone.getTimeZone("Europe/Amsterdam")
fun log(message: Any) {
//Log.d(TAG, message.toString())
println("$TAG $message")
}
fun scheduleJob(
context: Context,
time: Long,
executionTime: Long,
service: Class<*>,
extras: PersistableBundle = PersistableBundle(),
persist: Boolean? = null
) {
val serviceComponent = ComponentName(context, service)
val maximumLatency = timeUntil(time)
log("Max: $maximumLatency")
val builder = JobInfo.Builder((Math.random() * Int.MAX_VALUE).toInt(),
serviceComponent).apply {
setMinimumLatency(maximumLatency - executionTime)
setOverrideDeadline(maximumLatency)
setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
//setBackoffCriteria(executionTime, JobInfo.BACKOFF_POLICY_LINEAR)
setExtras(extras)
}
if (persist != null) builder.setPersisted(persist)
ContextCompat.getSystemService(context, JobScheduler::class.java)?.schedule(builder.build())
}
private fun timeUntil(time: Long): Long {
val now = Calendar.getInstance(TIMEZONE)
val plannedDate = Calendar.getInstance(TIMEZONE)
plannedDate.timeInMillis = time
val difference = plannedDate.timeInMillis - now.timeInMillis
return if (difference < 0) 0 else difference
}
}
}
Full source in case you would like to help / build it yourself