From 12b13f98f832d17fc54353160c6ee158cf3f8617 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Sat, 11 Apr 2020 11:03:13 +0300 Subject: [PATCH] Migrate to WorkManager --- app/build.gradle | 1 + .../java/org/koitharu/kotatsu/KotatsuApp.kt | 5 +- .../kotatsu/ui/common/BaseJobService.kt | 39 ---- .../koitharu/kotatsu/ui/main/MainActivity.kt | 2 + .../kotatsu/ui/tracker/TrackWorker.kt | 198 +++++++++++++++++ .../kotatsu/ui/tracker/TrackerJobService.kt | 209 ------------------ build.gradle | 2 +- 7 files changed, 205 insertions(+), 251 deletions(-) delete mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/common/BaseJobService.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackWorker.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackerJobService.kt diff --git a/app/build.gradle b/app/build.gradle index 3f42b93bd..7f34eb645 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,6 +69,7 @@ dependencies { implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha02' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.preference:preference:1.1.0' + implementation 'androidx.work:work-runtime-ktx:2.3.4' implementation 'com.google.android.material:material:1.2.0-alpha05' implementation 'androidx.room:room-runtime:2.2.5' diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 54b8152d8..49cbf779d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -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) } diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseJobService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseJobService.kt deleted file mode 100644 index 3be358a6e..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/ui/common/BaseJobService.kt +++ /dev/null @@ -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(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) -} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt index 9529d739e..f68e9558f 100644 --- a/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/ui/main/MainActivity.kt @@ -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() { diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackWorker.kt b/app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackWorker.kt new file mode 100644 index 000000000..f61105c14 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackWorker.kt @@ -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() + + 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) { + 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(4, TimeUnit.HOURS) + .setConstraints(constraints) + .addTag(TAG) + .setBackoffCriteria(BackoffPolicy.LINEAR, 30, TimeUnit.MINUTES) + .build() + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackerJobService.kt b/app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackerJobService.kt deleted file mode 100644 index 39501aa67..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/ui/tracker/TrackerJobService.kt +++ /dev/null @@ -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() - - 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) { - 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()) - } - } -} \ No newline at end of file diff --git a/build.gradle b/build.gradle index aebf163b7..30ef65d92 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0-alpha04' + classpath 'com.android.tools.build:gradle:4.1.0-alpha05' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong