Migrate to WorkManager
parent
0be4f56538
commit
12b13f98f8
@ -1,39 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.ui.common
|
|
||||||
|
|
||||||
import android.app.job.JobParameters
|
|
||||||
import android.app.job.JobService
|
|
||||||
import android.util.SparseArray
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.core.util.set
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
|
|
||||||
abstract class BaseJobService : JobService() {
|
|
||||||
|
|
||||||
private val jobServiceScope = object : CoroutineScope {
|
|
||||||
override val coroutineContext = Dispatchers.Main + SupervisorJob()
|
|
||||||
}
|
|
||||||
private val jobs = SparseArray<Job>(2)
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onStartJob(params: JobParameters): Boolean {
|
|
||||||
jobs[params.jobId] = jobServiceScope.launch {
|
|
||||||
val isSuccess = try {
|
|
||||||
doWork(params)
|
|
||||||
true
|
|
||||||
} catch (_: Throwable) {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
jobFinished(params, !isSuccess)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onStopJob(params: JobParameters): Boolean {
|
|
||||||
val job = jobs[params.jobId] ?: return false
|
|
||||||
return !job.isCompleted
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(Throwable::class)
|
|
||||||
protected abstract suspend fun doWork(params: JobParameters)
|
|
||||||
}
|
|
||||||
@ -0,0 +1,198 @@
|
|||||||
|
package org.koitharu.kotatsu.ui.tracker
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import androidx.work.*
|
||||||
|
import coil.Coil
|
||||||
|
import coil.api.get
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.core.KoinComponent
|
||||||
|
import org.koin.core.inject
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.core.model.Manga
|
||||||
|
import org.koitharu.kotatsu.core.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
|
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
||||||
|
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
|
||||||
|
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
|
||||||
|
import org.koitharu.kotatsu.utils.ext.safe
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class TrackWorker(context: Context, workerParams: WorkerParameters) :
|
||||||
|
CoroutineWorker(context, workerParams), KoinComponent {
|
||||||
|
|
||||||
|
private val notificationManager by lazy(LazyThreadSafetyMode.NONE) {
|
||||||
|
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
}
|
||||||
|
|
||||||
|
private val settings by inject<AppSettings>()
|
||||||
|
|
||||||
|
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||||
|
val repo = TrackingRepository()
|
||||||
|
val tracks = repo.getAllTracks()
|
||||||
|
if (tracks.isEmpty()) {
|
||||||
|
return@withContext Result.success()
|
||||||
|
}
|
||||||
|
var success = 0
|
||||||
|
for (track in tracks) {
|
||||||
|
val details = safe {
|
||||||
|
MangaProviderFactory.create(track.manga.source)
|
||||||
|
.getDetails(track.manga)
|
||||||
|
}
|
||||||
|
val chapters = details?.chapters ?: continue
|
||||||
|
when {
|
||||||
|
track.knownChaptersCount == -1 -> { //first check
|
||||||
|
repo.storeTrackResult(
|
||||||
|
mangaId = track.manga.id,
|
||||||
|
knownChaptersCount = chapters.size,
|
||||||
|
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
||||||
|
lastNotifiedChapterId = 0L,
|
||||||
|
newChapters = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { //manga was empty on last check
|
||||||
|
repo.storeTrackResult(
|
||||||
|
mangaId = track.manga.id,
|
||||||
|
knownChaptersCount = track.knownChaptersCount,
|
||||||
|
lastChapterId = 0L,
|
||||||
|
lastNotifiedChapterId = chapters.lastOrNull()?.id ?: 0L,
|
||||||
|
newChapters = chapters.size
|
||||||
|
)
|
||||||
|
showNotification(track.manga, chapters)
|
||||||
|
}
|
||||||
|
chapters.size == track.knownChaptersCount -> {
|
||||||
|
if (chapters.lastOrNull()?.id == track.lastChapterId) {
|
||||||
|
// manga was not updated. skip
|
||||||
|
} else {
|
||||||
|
// number of chapters still the same, bu last chapter changed.
|
||||||
|
// maybe some chapters are removed. we need to find last known chapter
|
||||||
|
val knownChapter = chapters.indexOfLast { it.id == track.lastChapterId }
|
||||||
|
if (knownChapter == -1) {
|
||||||
|
// confuse. reset anything
|
||||||
|
repo.storeTrackResult(
|
||||||
|
mangaId = track.manga.id,
|
||||||
|
knownChaptersCount = chapters.size,
|
||||||
|
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
||||||
|
lastNotifiedChapterId = chapters.lastOrNull()?.id ?: 0L,
|
||||||
|
newChapters = 0
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val newChapters = chapters.size - knownChapter + 1
|
||||||
|
repo.storeTrackResult(
|
||||||
|
mangaId = track.manga.id,
|
||||||
|
knownChaptersCount = knownChapter + 1,
|
||||||
|
lastChapterId = track.lastChapterId,
|
||||||
|
lastNotifiedChapterId = chapters.lastOrNull()?.id ?: 0L,
|
||||||
|
newChapters = newChapters
|
||||||
|
)
|
||||||
|
if (chapters.lastOrNull()?.id != track.lastNotifiedChapterId) {
|
||||||
|
showNotification(track.manga, chapters.takeLast(newChapters))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val newChapters = chapters.size - track.knownChaptersCount
|
||||||
|
repo.storeTrackResult(
|
||||||
|
mangaId = track.manga.id,
|
||||||
|
knownChaptersCount = track.knownChaptersCount,
|
||||||
|
lastChapterId = track.lastChapterId,
|
||||||
|
lastNotifiedChapterId = chapters.lastOrNull()?.id ?: 0L,
|
||||||
|
newChapters = newChapters
|
||||||
|
)
|
||||||
|
if (chapters.lastOrNull()?.id != track.lastNotifiedChapterId) {
|
||||||
|
showNotification(track.manga, chapters.takeLast(newChapters))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
success++
|
||||||
|
}
|
||||||
|
if (success == 0) {
|
||||||
|
Result.retry()
|
||||||
|
} else {
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun showNotification(manga: Manga, newChapters: List<MangaChapter>) {
|
||||||
|
if (newChapters.isEmpty() || !settings.trackerNotifications) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val id = manga.url.hashCode()
|
||||||
|
val colorPrimary = ContextCompat.getColor(applicationContext, R.color.blue_primary)
|
||||||
|
val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||||
|
val summary = applicationContext.resources.getQuantityString(R.plurals.new_chapters,
|
||||||
|
newChapters.size, newChapters.size)
|
||||||
|
with(builder) {
|
||||||
|
setContentText(summary)
|
||||||
|
setContentText(manga.title)
|
||||||
|
setNumber(newChapters.size)
|
||||||
|
setLargeIcon(safe {
|
||||||
|
Coil.loader().get(manga.coverUrl).toBitmap()
|
||||||
|
})
|
||||||
|
setSmallIcon(R.drawable.ic_stat_book_plus)
|
||||||
|
val style = NotificationCompat.InboxStyle(this)
|
||||||
|
for (chapter in newChapters) {
|
||||||
|
style.addLine(chapter.name)
|
||||||
|
}
|
||||||
|
style.setSummaryText(manga.title)
|
||||||
|
style.setBigContentTitle(summary)
|
||||||
|
setStyle(style)
|
||||||
|
val intent = MangaDetailsActivity.newIntent(applicationContext, manga)
|
||||||
|
setContentIntent(PendingIntent.getActivity(applicationContext, id,
|
||||||
|
intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
||||||
|
setAutoCancel(true)
|
||||||
|
color = colorPrimary
|
||||||
|
setLights(colorPrimary, 1000, 5000)
|
||||||
|
setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
notificationManager.notify(TAG, id, builder.build())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val CHANNEL_ID = "tracking"
|
||||||
|
private const val TAG = "tracking"
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun createNotificationChannel(context: Context) {
|
||||||
|
val manager =
|
||||||
|
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
|
||||||
|
val channel = NotificationChannel(CHANNEL_ID,
|
||||||
|
context.getString(R.string.new_chapters),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT)
|
||||||
|
channel.setShowBadge(true)
|
||||||
|
channel.lightColor = ContextCompat.getColor(context, R.color.blue_primary)
|
||||||
|
channel.enableLights(true)
|
||||||
|
manager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setup(context: Context) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
createNotificationChannel(context)
|
||||||
|
}
|
||||||
|
val constraints = Constraints.Builder()
|
||||||
|
.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||||
|
.build()
|
||||||
|
val request = PeriodicWorkRequestBuilder<TrackWorker>(4, TimeUnit.HOURS)
|
||||||
|
.setConstraints(constraints)
|
||||||
|
.addTag(TAG)
|
||||||
|
.setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context)
|
||||||
|
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,209 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.ui.tracker
|
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.job.JobInfo
|
|
||||||
import android.app.job.JobParameters
|
|
||||||
import android.app.job.JobScheduler
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Build
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
|
||||||
import coil.Coil
|
|
||||||
import coil.api.get
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import org.koin.android.ext.android.inject
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.core.model.Manga
|
|
||||||
import org.koitharu.kotatsu.core.model.MangaChapter
|
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
|
||||||
import org.koitharu.kotatsu.domain.MangaProviderFactory
|
|
||||||
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
|
|
||||||
import org.koitharu.kotatsu.ui.common.BaseJobService
|
|
||||||
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
|
|
||||||
import org.koitharu.kotatsu.utils.ext.safe
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class TrackerJobService : BaseJobService() {
|
|
||||||
|
|
||||||
private val notificationManager by lazy(LazyThreadSafetyMode.NONE) {
|
|
||||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
}
|
|
||||||
|
|
||||||
private val settings by inject<AppSettings>()
|
|
||||||
|
|
||||||
override suspend fun doWork(params: JobParameters) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val repo = TrackingRepository()
|
|
||||||
val tracks = repo.getAllTracks()
|
|
||||||
if (tracks.isEmpty()) {
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
var success = 0
|
|
||||||
for (track in tracks) {
|
|
||||||
val details = safe {
|
|
||||||
MangaProviderFactory.create(track.manga.source)
|
|
||||||
.getDetails(track.manga)
|
|
||||||
}
|
|
||||||
val chapters = details?.chapters ?: continue
|
|
||||||
when {
|
|
||||||
track.knownChaptersCount == -1 -> { //first check
|
|
||||||
repo.storeTrackResult(
|
|
||||||
mangaId = track.manga.id,
|
|
||||||
knownChaptersCount = chapters.size,
|
|
||||||
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
|
||||||
lastNotifiedChapterId = 0L,
|
|
||||||
newChapters = 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
track.knownChaptersCount == 0 && track.lastChapterId == 0L -> { //manga was empty on last check
|
|
||||||
repo.storeTrackResult(
|
|
||||||
mangaId = track.manga.id,
|
|
||||||
knownChaptersCount = track.knownChaptersCount,
|
|
||||||
lastChapterId = 0L,
|
|
||||||
lastNotifiedChapterId = chapters.lastOrNull()?.id ?: 0L,
|
|
||||||
newChapters = chapters.size
|
|
||||||
)
|
|
||||||
showNotification(track.manga, chapters)
|
|
||||||
}
|
|
||||||
chapters.size == track.knownChaptersCount -> {
|
|
||||||
if (chapters.lastOrNull()?.id == track.lastChapterId) {
|
|
||||||
// manga was not updated. skip
|
|
||||||
} else {
|
|
||||||
// number of chapters still the same, bu last chapter changed.
|
|
||||||
// maybe some chapters are removed. we need to find last known chapter
|
|
||||||
val knownChapter = chapters.indexOfLast { it.id == track.lastChapterId }
|
|
||||||
if (knownChapter == -1) {
|
|
||||||
// confuse. reset anything
|
|
||||||
repo.storeTrackResult(
|
|
||||||
mangaId = track.manga.id,
|
|
||||||
knownChaptersCount = chapters.size,
|
|
||||||
lastChapterId = chapters.lastOrNull()?.id ?: 0L,
|
|
||||||
lastNotifiedChapterId = chapters.lastOrNull()?.id ?: 0L,
|
|
||||||
newChapters = 0
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
val newChapters = chapters.size - knownChapter + 1
|
|
||||||
repo.storeTrackResult(
|
|
||||||
mangaId = track.manga.id,
|
|
||||||
knownChaptersCount = knownChapter + 1,
|
|
||||||
lastChapterId = track.lastChapterId,
|
|
||||||
lastNotifiedChapterId = chapters.lastOrNull()?.id ?: 0L,
|
|
||||||
newChapters = newChapters
|
|
||||||
)
|
|
||||||
if (chapters.lastOrNull()?.id != track.lastNotifiedChapterId) {
|
|
||||||
showNotification(track.manga, chapters.takeLast(newChapters))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
val newChapters = chapters.size - track.knownChaptersCount
|
|
||||||
repo.storeTrackResult(
|
|
||||||
mangaId = track.manga.id,
|
|
||||||
knownChaptersCount = track.knownChaptersCount,
|
|
||||||
lastChapterId = track.lastChapterId,
|
|
||||||
lastNotifiedChapterId = chapters.lastOrNull()?.id ?: 0L,
|
|
||||||
newChapters = newChapters
|
|
||||||
)
|
|
||||||
if (chapters.lastOrNull()?.id != track.lastNotifiedChapterId) {
|
|
||||||
showNotification(track.manga, chapters.takeLast(newChapters))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
success++
|
|
||||||
}
|
|
||||||
if (success == 0) {
|
|
||||||
throw RuntimeException("Cannot check any manga updates")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun showNotification(manga: Manga, newChapters: List<MangaChapter>) {
|
|
||||||
if (newChapters.isEmpty() || !settings.trackerNotifications) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val id = manga.url.hashCode()
|
|
||||||
val colorPrimary = ContextCompat.getColor(this@TrackerJobService, R.color.blue_primary)
|
|
||||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
|
||||||
val summary = resources.getQuantityString(R.plurals.new_chapters,
|
|
||||||
newChapters.size, newChapters.size)
|
|
||||||
with(builder) {
|
|
||||||
setContentText(summary)
|
|
||||||
setContentText(manga.title)
|
|
||||||
setNumber(newChapters.size)
|
|
||||||
setLargeIcon(safe {
|
|
||||||
Coil.loader().get(manga.coverUrl).toBitmap()
|
|
||||||
})
|
|
||||||
setSmallIcon(R.drawable.ic_stat_book_plus)
|
|
||||||
val style = NotificationCompat.InboxStyle(this)
|
|
||||||
for (chapter in newChapters) {
|
|
||||||
style.addLine(chapter.name)
|
|
||||||
}
|
|
||||||
style.setSummaryText(manga.title)
|
|
||||||
style.setBigContentTitle(summary)
|
|
||||||
setStyle(style)
|
|
||||||
val intent = MangaDetailsActivity.newIntent(this@TrackerJobService, manga)
|
|
||||||
setContentIntent(PendingIntent.getActivity(this@TrackerJobService, id,
|
|
||||||
intent, PendingIntent.FLAG_UPDATE_CURRENT))
|
|
||||||
setAutoCancel(true)
|
|
||||||
color = colorPrimary
|
|
||||||
setLights(colorPrimary, 1000, 5000)
|
|
||||||
setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
notificationManager.notify(TAG, id, builder.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
private const val JOB_ID = 7
|
|
||||||
private const val CHANNEL_ID = "tracking"
|
|
||||||
private const val TAG = "tracking"
|
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
private fun createNotificationChannel(context: Context) {
|
|
||||||
val manager =
|
|
||||||
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
||||||
if (manager.getNotificationChannel(CHANNEL_ID) == null) {
|
|
||||||
val channel = NotificationChannel(CHANNEL_ID,
|
|
||||||
context.getString(R.string.new_chapters),
|
|
||||||
NotificationManager.IMPORTANCE_DEFAULT)
|
|
||||||
channel.setShowBadge(true)
|
|
||||||
channel.lightColor = ContextCompat.getColor(context, R.color.blue_primary)
|
|
||||||
channel.enableLights(true)
|
|
||||||
manager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setup(context: Context) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
createNotificationChannel(context)
|
|
||||||
}
|
|
||||||
val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
|
|
||||||
if (scheduler.allPendingJobs.any { it.id == JOB_ID }) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val jobInfo =
|
|
||||||
JobInfo.Builder(JOB_ID, ComponentName(context, TrackerJobService::class.java))
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
jobInfo.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NOT_ROAMING)
|
|
||||||
} else {
|
|
||||||
jobInfo.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
jobInfo.setRequiresBatteryNotLow(true)
|
|
||||||
}
|
|
||||||
jobInfo.setRequiresDeviceIdle(true)
|
|
||||||
jobInfo.setPersisted(true)
|
|
||||||
jobInfo.setPeriodic(TimeUnit.HOURS.toMillis(4))
|
|
||||||
scheduler.schedule(jobInfo.build())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue