Migrate to WorkManager

This commit is contained in:
Koitharu
2020-04-11 11:03:13 +03:00
parent 0be4f56538
commit 12b13f98f8
7 changed files with 205 additions and 251 deletions

View File

@@ -25,7 +25,6 @@ import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePers
import org.koitharu.kotatsu.core.parser.UserAgentInterceptor
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.ui.tracker.TrackerJobService
import org.koitharu.kotatsu.ui.utils.AppCrashHandler
import org.koitharu.kotatsu.utils.CacheUtils
import java.util.concurrent.TimeUnit
@@ -45,7 +44,9 @@ class KotatsuApp : Application() {
initKoin()
initCoil()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
TrackerJobService.setup(this)
if (BuildConfig.DEBUG) {
initErrorHandler()
}
AppCompatDelegate.setDefaultNightMode(AppSettings(this).theme)
}

View File

@@ -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)
}

View File

@@ -30,6 +30,7 @@ import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.settings.AppUpdateService
import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.ui.tracker.TrackWorker
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.resolveDp
@@ -69,6 +70,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
drawer.postDelayed(2000) {
AppUpdateService.startIfRequired(applicationContext)
}
TrackWorker.setup(applicationContext)
}
override fun onDestroy() {

View File

@@ -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)
}
}
}

View File

@@ -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())
}
}
}