mirror of https://github.com/tasks/tasks
Refactor notification scheduling
* Remove foreground service * Use expedited work to trigger notifications * Remove miscellaneous notification channelpull/2872/head
parent
95c351e9fd
commit
3cd0295b71
@ -1,73 +0,0 @@
|
||||
package org.tasks.injection
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import androidx.core.app.NotificationCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tasks.R
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.notifications.NotificationManager
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class InjectingService : Service() {
|
||||
private val job = SupervisorJob()
|
||||
private val scope = CoroutineScope(Dispatchers.Default + job)
|
||||
|
||||
@Inject lateinit var firebase: Firebase
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startForeground()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
startForeground()
|
||||
scope.launch {
|
||||
try {
|
||||
doWork()
|
||||
} catch (e: Exception) {
|
||||
firebase.reportException(e)
|
||||
} finally {
|
||||
done(startId)
|
||||
}
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun done(startId: Int) {
|
||||
scheduleNext()
|
||||
stopSelf(startId)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
stopForeground(true)
|
||||
job.cancel()
|
||||
}
|
||||
|
||||
private fun startForeground() {
|
||||
startForeground(notificationId, buildNotification())
|
||||
}
|
||||
|
||||
protected abstract val notificationId: Int
|
||||
protected abstract val notificationBody: Int
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
return NotificationCompat.Builder(
|
||||
this, NotificationManager.NOTIFICATION_CHANNEL_MISCELLANEOUS)
|
||||
.setSound(null)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setSmallIcon(R.drawable.ic_check_white_24dp)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setContentText(getString(notificationBody))
|
||||
.build()
|
||||
}
|
||||
|
||||
protected open fun scheduleNext() {}
|
||||
|
||||
protected abstract suspend fun doWork()
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
package org.tasks.jobs
|
||||
|
||||
import com.google.common.collect.Ordering
|
||||
import com.google.common.collect.TreeMultimap
|
||||
import com.google.common.primitives.Ints
|
||||
import org.tasks.preferences.Preferences
|
||||
import org.tasks.time.DateTime
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NotificationQueue @Inject constructor(
|
||||
private val preferences: Preferences,
|
||||
private val workManager: WorkManager
|
||||
) {
|
||||
private val jobs =
|
||||
TreeMultimap.create<Long, AlarmEntry>(Ordering.natural()) { l, r ->
|
||||
Ints.compare(l.hashCode(), r.hashCode())
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun add(entry: AlarmEntry) = add(listOf(entry))
|
||||
|
||||
@Synchronized
|
||||
fun add(entries: Iterable<AlarmEntry>) {
|
||||
val originalFirstTime = firstTime()
|
||||
entries.forEach { jobs.put(it.time, it) }
|
||||
if (originalFirstTime != firstTime()) {
|
||||
scheduleNext(true)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
jobs.clear()
|
||||
workManager.cancelNotifications()
|
||||
}
|
||||
|
||||
fun cancelForTask(taskId: Long) {
|
||||
val firstTime = firstTime()
|
||||
jobs.values().filter { it.taskId == taskId }.forEach { remove(listOf(it)) }
|
||||
if (firstTime != firstTime()) {
|
||||
scheduleNext(true)
|
||||
}
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
val overdueJobs: List<AlarmEntry>
|
||||
get() = jobs.keySet()
|
||||
.headSet(DateTime().startOfMinute().plusMinutes(1).millis)
|
||||
.flatMap { jobs[it] }
|
||||
|
||||
@Synchronized
|
||||
fun scheduleNext() = scheduleNext(false)
|
||||
|
||||
private fun scheduleNext(cancelCurrent: Boolean) {
|
||||
if (jobs.isEmpty) {
|
||||
if (cancelCurrent) {
|
||||
workManager.cancelNotifications()
|
||||
}
|
||||
} else {
|
||||
workManager.scheduleNotification(nextScheduledTime())
|
||||
}
|
||||
}
|
||||
|
||||
private fun firstTime() = if (jobs.isEmpty) 0L else jobs.asMap().firstKey()
|
||||
|
||||
fun nextScheduledTime(): Long {
|
||||
val next = firstTime()
|
||||
return if (next > 0) preferences.adjustForQuietHours(next) else 0
|
||||
}
|
||||
|
||||
fun size() = jobs.size()
|
||||
|
||||
fun getJobs() = jobs.values().toList()
|
||||
|
||||
fun isEmpty() = jobs.isEmpty
|
||||
|
||||
@Synchronized
|
||||
fun remove(entries: List<AlarmEntry>): Boolean {
|
||||
var success = true
|
||||
for (entry in entries) {
|
||||
success = success and (!jobs.containsEntry(entry.time, entry) || jobs.remove(
|
||||
entry.time,
|
||||
entry
|
||||
))
|
||||
}
|
||||
return success
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package org.tasks.jobs
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationReceiver: BroadcastReceiver() {
|
||||
@Inject lateinit var workManager: WorkManager
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
workManager.triggerNotifications(expedited = true)
|
||||
}
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
package org.tasks.jobs
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import com.todoroo.andlib.utility.AndroidUtilities
|
||||
import com.todoroo.astrid.alarms.AlarmService
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.tasks.Notifier
|
||||
import org.tasks.R
|
||||
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
|
||||
import org.tasks.data.AlarmDao
|
||||
import org.tasks.injection.InjectingService
|
||||
import org.tasks.preferences.Preferences
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class NotificationService : InjectingService() {
|
||||
@Inject lateinit var preferences: Preferences
|
||||
@Inject lateinit var notifier: Notifier
|
||||
@Inject lateinit var notificationQueue: NotificationQueue
|
||||
@Inject lateinit var alarmDao: AlarmDao
|
||||
@Inject lateinit var alarmService: AlarmService
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? = null
|
||||
|
||||
override val notificationId = -1
|
||||
|
||||
override val notificationBody = R.string.building_notifications
|
||||
|
||||
override suspend fun doWork() {
|
||||
AndroidUtilities.assertNotMainThread()
|
||||
if (!preferences.isCurrentlyQuietHours) {
|
||||
val overdueJobs = notificationQueue.overdueJobs
|
||||
if (!notificationQueue.remove(overdueJobs)) {
|
||||
throw RuntimeException("Failed to remove jobs from queue")
|
||||
}
|
||||
notifier.triggerNotifications(overdueJobs.map { it.toNotification() })
|
||||
overdueJobs
|
||||
.filter { it.type == TYPE_SNOOZE }
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.map { it.id }
|
||||
?.let { alarmDao.deleteByIds(it) }
|
||||
overdueJobs
|
||||
.map { it.taskId }
|
||||
.let { alarmService.scheduleAlarms(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun scheduleNext() = notificationQueue.scheduleNext()
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package org.tasks.jobs
|
||||
|
||||
import android.content.Context
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.todoroo.andlib.utility.DateUtilities.now
|
||||
import com.todoroo.astrid.alarms.AlarmService
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.tasks.Notifier
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
|
||||
import org.tasks.data.AlarmDao
|
||||
import org.tasks.date.DateTimeUtils.toDateTime
|
||||
import org.tasks.preferences.Preferences
|
||||
import timber.log.Timber
|
||||
|
||||
@HiltWorker
|
||||
class NotificationWork @AssistedInject constructor(
|
||||
@Assisted context: Context,
|
||||
@Assisted workerParams: WorkerParameters,
|
||||
firebase: Firebase,
|
||||
private val workManager: WorkManager,
|
||||
private val alarmService: AlarmService,
|
||||
private val alarmDao: AlarmDao,
|
||||
private val preferences: Preferences,
|
||||
private val notifier: Notifier,
|
||||
) : RepeatingWorker(context, workerParams, firebase) {
|
||||
private var nextAlarm: Long = 0
|
||||
|
||||
override suspend fun run(): Result {
|
||||
if (preferences.isCurrentlyQuietHours) {
|
||||
nextAlarm = preferences.adjustForQuietHours(now())
|
||||
return Result.success()
|
||||
}
|
||||
repeat(3) {
|
||||
val (overdue, future) = alarmService.getAlarms()
|
||||
if (overdue.isNotEmpty()) {
|
||||
overdue
|
||||
.sortedBy { it.time }
|
||||
.also { alarms ->
|
||||
alarms
|
||||
.filter { it.type == TYPE_SNOOZE }
|
||||
.map { it.id }
|
||||
.let { alarmDao.deleteByIds(it) }
|
||||
}
|
||||
.map { it.toNotification() }
|
||||
.let { notifier.triggerNotifications(it) }
|
||||
} else {
|
||||
nextAlarm = future.minOfOrNull { it.time } ?: 0
|
||||
Timber.d("nextAlarm=${nextAlarm.toDateTime()}")
|
||||
return Result.success()
|
||||
}
|
||||
}
|
||||
firebase.reportException(IllegalStateException("Should have returned already"))
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
override suspend fun scheduleNext() {
|
||||
if (nextAlarm > 0) {
|
||||
workManager.scheduleNotification(preferences.adjustForQuietHours(nextAlarm))
|
||||
}
|
||||
}
|
||||
}
|
@ -1,263 +0,0 @@
|
||||
package org.tasks.jobs
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.AdditionalAnswers
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mockito
|
||||
import org.tasks.Freeze.Companion.freezeAt
|
||||
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
|
||||
import org.tasks.data.Alarm.Companion.TYPE_SNOOZE
|
||||
import org.tasks.preferences.Preferences
|
||||
import org.tasks.time.DateTime
|
||||
import org.tasks.time.DateTimeUtils
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NotificationQueueTest {
|
||||
private lateinit var queue: NotificationQueue
|
||||
private lateinit var workManager: WorkManager
|
||||
private lateinit var preferences: Preferences
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
preferences = Mockito.mock(Preferences::class.java)
|
||||
Mockito.`when`(preferences.adjustForQuietHours(ArgumentMatchers.anyLong()))
|
||||
.then(AdditionalAnswers.returnsFirstArg<Any>())
|
||||
workManager = Mockito.mock(WorkManager::class.java)
|
||||
queue = NotificationQueue(preferences, workManager)
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
Mockito.verifyNoMoreInteractions(workManager)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeAlarmDoesntAffectOtherAlarm() {
|
||||
val now = DateTimeUtils.currentTimeMillis()
|
||||
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(now)
|
||||
queue.add(AlarmEntry(2, 2, now, TYPE_DATE_TIME))
|
||||
queue.remove(listOf(AlarmEntry(1, 1, now, TYPE_DATE_TIME)))
|
||||
freezeAt(now) {
|
||||
assertEquals(
|
||||
listOf(AlarmEntry(2, 2, now, TYPE_DATE_TIME)),
|
||||
queue.overdueJobs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removeByTaskDoesntAffectOtherAlarm() {
|
||||
val now = DateTimeUtils.currentTimeMillis()
|
||||
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(now)
|
||||
queue.add(AlarmEntry(2, 2, now, TYPE_DATE_TIME))
|
||||
queue.cancelForTask(1)
|
||||
freezeAt(now) {
|
||||
assertEquals(
|
||||
listOf(AlarmEntry(2, 2, now, TYPE_DATE_TIME)),
|
||||
queue.overdueJobs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rescheduleForFirstJob() {
|
||||
queue.add(AlarmEntry(1, 2, 3, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dontRescheduleForLaterJobs() {
|
||||
queue.add(AlarmEntry(1, 2, 3, TYPE_DATE_TIME))
|
||||
queue.add(AlarmEntry(2, 3, 4, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(3)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rescheduleForNewerJob() {
|
||||
queue.add(AlarmEntry(1, 1, 2, TYPE_DATE_TIME))
|
||||
queue.add(AlarmEntry(1, 1, 1, TYPE_DATE_TIME))
|
||||
val order = Mockito.inOrder(workManager)
|
||||
order.verify(workManager).scheduleNotification(2)
|
||||
order.verify(workManager).scheduleNotification(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rescheduleWhenCancelingOnlyJob() {
|
||||
queue.add(AlarmEntry(1, 1, 2, TYPE_DATE_TIME))
|
||||
queue.cancelForTask(1)
|
||||
val order = Mockito.inOrder(workManager)
|
||||
order.verify(workManager).scheduleNotification(2)
|
||||
order.verify(workManager).cancelNotifications()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rescheduleWhenCancelingFirstJob() {
|
||||
queue.add(AlarmEntry(1, 1, 1, 0))
|
||||
queue.add(AlarmEntry(2, 2, 2, 0))
|
||||
queue.cancelForTask(1)
|
||||
val order = Mockito.inOrder(workManager)
|
||||
order.verify(workManager).scheduleNotification(1)
|
||||
order.verify(workManager).scheduleNotification(2)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun dontRescheduleWhenCancelingLaterJob() {
|
||||
queue.add(AlarmEntry(1, 1, 1, 0))
|
||||
queue.add(AlarmEntry(2, 2, 2, 0))
|
||||
queue.cancelForTask(2)
|
||||
Mockito.verify(workManager).scheduleNotification(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun nextScheduledTimeIsZeroWhenQueueIsEmpty() {
|
||||
Mockito.`when`(preferences.adjustForQuietHours(ArgumentMatchers.anyLong()))
|
||||
.thenReturn(1234L)
|
||||
assertEquals(0, queue.nextScheduledTime())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun adjustNextScheduledTimeForQuietHours() {
|
||||
Mockito.`when`(preferences.adjustForQuietHours(ArgumentMatchers.anyLong()))
|
||||
.thenReturn(1234L)
|
||||
queue.add(AlarmEntry(1, 1, 1, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(1234)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun overdueJobsAreReturned() {
|
||||
val now = DateTimeUtils.currentTimeMillis()
|
||||
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
|
||||
queue.add(AlarmEntry(2, 1, now + ONE_MINUTE, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(now)
|
||||
freezeAt(now) {
|
||||
assertEquals(
|
||||
listOf(AlarmEntry(1, 1, now, TYPE_DATE_TIME)), queue.overdueJobs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoOverdueJobsAtSameTimeReturned() {
|
||||
val now = DateTimeUtils.currentTimeMillis()
|
||||
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
|
||||
queue.add(AlarmEntry(2, 2, now, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(now)
|
||||
freezeAt(now) {
|
||||
assertEquals(
|
||||
setOf(
|
||||
AlarmEntry(1, 1, now, TYPE_DATE_TIME),
|
||||
AlarmEntry(2, 2, now, TYPE_DATE_TIME)
|
||||
),
|
||||
queue.overdueJobs.toSet()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun twoOverdueJobsAtDifferentTimes() {
|
||||
val now = DateTimeUtils.currentTimeMillis()
|
||||
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
|
||||
queue.add(AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(now)
|
||||
freezeAt(now + 2 * ONE_MINUTE) {
|
||||
assertEquals(
|
||||
listOf(
|
||||
AlarmEntry(1, 1, now, TYPE_DATE_TIME),
|
||||
AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME)
|
||||
),
|
||||
queue.overdueJobs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun overdueJobsAreRemoved() {
|
||||
val now = DateTimeUtils.currentTimeMillis()
|
||||
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
|
||||
queue.add(AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(now)
|
||||
freezeAt(now) {
|
||||
queue.remove(queue.overdueJobs)
|
||||
}
|
||||
assertEquals(
|
||||
listOf(AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME)), queue.getJobs()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun multipleOverduePeriodsLapsed() {
|
||||
val now = DateTimeUtils.currentTimeMillis()
|
||||
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
|
||||
queue.add(AlarmEntry(2, 2, now + ONE_MINUTE, TYPE_DATE_TIME))
|
||||
queue.add(AlarmEntry(3, 3, now + 2 * ONE_MINUTE, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(now)
|
||||
freezeAt(now + ONE_MINUTE) {
|
||||
queue.remove(queue.overdueJobs)
|
||||
}
|
||||
assertEquals(
|
||||
listOf(AlarmEntry(3, 3, now + 2 * ONE_MINUTE, TYPE_DATE_TIME)), queue.getJobs()
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun clearShouldCancelExisting() {
|
||||
queue.add(AlarmEntry(1, 1, 1, 0))
|
||||
queue.clear()
|
||||
val order = Mockito.inOrder(workManager)
|
||||
order.verify(workManager).scheduleNotification(1)
|
||||
order.verify(workManager).cancelNotifications()
|
||||
assertEquals(0, queue.size())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoreInvalidCancelForByAlarm() {
|
||||
val now = DateTimeUtils.currentTimeMillis()
|
||||
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
|
||||
queue.remove(listOf(AlarmEntry(2, 2, now, TYPE_DATE_TIME)))
|
||||
Mockito.verify(workManager).scheduleNotification(now)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ignoreInvalidCancelForTask() {
|
||||
val now = DateTimeUtils.currentTimeMillis()
|
||||
queue.add(AlarmEntry(1, 1, now, TYPE_DATE_TIME))
|
||||
queue.cancelForTask(2)
|
||||
Mockito.verify(workManager).scheduleNotification(now)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun allDuringSameMinuteAreOverdue() {
|
||||
val now = DateTime(2017, 9, 3, 0, 14, 6, 455)
|
||||
val due = DateTime(2017, 9, 3, 0, 14, 0, 0)
|
||||
val snooze = DateTime(2017, 9, 3, 0, 14, 59, 999)
|
||||
queue.add(AlarmEntry(1, 1, due.millis, TYPE_DATE_TIME))
|
||||
queue.add(AlarmEntry(2, 2, snooze.millis, TYPE_SNOOZE))
|
||||
queue.add(AlarmEntry(3, 3, due.plusMinutes(1).millis, TYPE_DATE_TIME))
|
||||
Mockito.verify(workManager).scheduleNotification(due.millis)
|
||||
freezeAt(now) {
|
||||
val overdueJobs = queue.overdueJobs
|
||||
assertEquals(
|
||||
listOf(
|
||||
AlarmEntry(1, 1, due.millis, TYPE_DATE_TIME),
|
||||
AlarmEntry(2, 2, snooze.millis, TYPE_SNOOZE)
|
||||
),
|
||||
overdueJobs
|
||||
)
|
||||
queue.remove(overdueJobs)
|
||||
assertEquals(
|
||||
listOf(AlarmEntry(3, 3, due.plusMinutes(1).millis, TYPE_DATE_TIME)),
|
||||
queue.getJobs()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val ONE_MINUTE = TimeUnit.MINUTES.toMillis(1)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue