3

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

Jason
  • 1,658
  • 3
  • 20
  • 51
  • See: [Alarmmanager not working after phone reboot](https://stackoverflow.com/q/41197416/295004) as WorkManager/JobScheduler won't give you what you want per: https://developer.android.com/guide/background?hl=en – Morrison Chang May 06 '19 at 20:21
  • I know about onBootReceive and I know I can easily persist them, but I would rather avoid this and use newer technologies. The job does not need to specifically run at that time, but it dpes however need to run within the given time, always. – Jason May 06 '19 at 20:25
  • You should include what are the background settings on the app, as well as which test device/OS version. I'm wondering if you are hitting the App Standby Bucket or even if the high frequency of wake ups is hitting the Doze deferment. – Morrison Chang May 06 '19 at 20:39
  • I do not quite understand what you mean with background settings, are you referring to the battery optimization? The device is a Samsung Galaxy S10 with Android Pie. It does, strangely, always work when I'm asleep (00:00 - 08:00) – Jason May 06 '19 at 20:50
  • Yes what are the battery settings on the app. Device is plugged in and charging, so battery restrictions are relaxed. I would at least increase the period >15 minutes per [JobScheduler not repeating job](https://stackoverflow.com/a/49837688/295004) – Morrison Chang May 06 '19 at 20:51
  • Background activity is allowed in the background settings, though ofcourse the app is still battery optimized by default (as happens to almost any non-default app). Currently late at my timezone so I can't respond any time soon but I will try to see if 15 minutes periods does not cut out the service, thank you – Jason May 06 '19 at 21:14
  • Set it to work every 30, but even that does not always work. I guess I just have to use the AlarmManager after all.. :( – Jason May 09 '19 at 09:00

0 Answers0