diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt index 0422b48c8..958dba4f9 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Flow.kt @@ -11,8 +11,10 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.transformLatest import org.koitharu.kotatsu.R +import java.util.concurrent.atomic.AtomicInteger fun Flow.onFirst(action: suspend (T) -> Unit): Flow { var isFirstCall = true @@ -37,6 +39,14 @@ fun Flow.onEachWhile(action: suspend (T) -> Boolean): Flow { } } +fun Flow.onEachIndexed(action: suspend (index: Int, T) -> Unit): Flow { + val counter = AtomicInteger(0) + return transform { value -> + action(counter.getAndIncrement(), value) + return@transform emit(value) + } +} + inline fun Flow>.mapItems(crossinline transform: (T) -> R): Flow> { return map { list -> list.map(transform) } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt index b7e661358..e3915b4cf 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/suggestions/ui/SuggestionsWorker.kt @@ -68,6 +68,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.reader.ui.ReaderActivity.IntentBuilder +import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler import org.koitharu.kotatsu.suggestions.domain.MangaSuggestion import org.koitharu.kotatsu.suggestions.domain.SuggestionRepository @@ -76,6 +77,7 @@ import java.util.concurrent.TimeUnit import javax.inject.Inject import kotlin.math.pow import kotlin.random.Random +import com.google.android.material.R as materialR @HiltWorker class SuggestionsWorker @AssistedInject constructor( @@ -86,6 +88,7 @@ class SuggestionsWorker @AssistedInject constructor( private val historyRepository: HistoryRepository, private val favouritesRepository: FavouritesRepository, private val appSettings: AppSettings, + private val workManager: WorkManager, private val mangaRepositoryFactory: MangaRepository.Factory, private val sourcesRepository: MangaSourcesRepository, ) : CoroutineWorker(appContext, params) { @@ -116,6 +119,19 @@ class SuggestionsWorker @AssistedInject constructor( val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID) .setContentTitle(title) + .setContentIntent( + PendingIntentCompat.getActivity( + applicationContext, + 0, + SettingsActivity.newSuggestionsSettingsIntent(applicationContext), + 0, + false, + ), + ).addAction( + materialR.drawable.material_ic_clear_black_24dp, + applicationContext.getString(android.R.string.cancel), + workManager.createCancelPendingIntent(id), + ) .setPriority(NotificationCompat.PRIORITY_MIN) .setCategory(NotificationCompat.CATEGORY_SERVICE) .setDefaults(0) @@ -123,7 +139,13 @@ class SuggestionsWorker @AssistedInject constructor( .setSilent(true) .setProgress(0, 0, true) .setSmallIcon(android.R.drawable.stat_notify_sync) - .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED) + .setForegroundServiceBehavior( + if (TAG_ONESHOT in tags) { + NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE + } else { + NotificationCompat.FOREGROUND_SERVICE_DEFERRED + }, + ) .build() return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt index a837b5e0b..c868eb610 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/work/TrackWorker.kt @@ -39,7 +39,6 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.toList import kotlinx.coroutines.launch import kotlinx.coroutines.runInterruptible @@ -54,6 +53,7 @@ import org.koitharu.kotatsu.core.logs.TrackerLogger import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission +import org.koitharu.kotatsu.core.util.ext.onEachIndexed import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.details.ui.DetailsActivity @@ -61,11 +61,13 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.runCatchingCancellable +import org.koitharu.kotatsu.settings.SettingsActivity import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler import org.koitharu.kotatsu.tracker.domain.Tracker import org.koitharu.kotatsu.tracker.domain.model.MangaUpdates import java.util.concurrent.TimeUnit import javax.inject.Inject +import com.google.android.material.R as materialR @HiltWorker class TrackWorker @AssistedInject constructor( @@ -74,6 +76,7 @@ class TrackWorker @AssistedInject constructor( private val coil: ImageLoader, private val settings: AppSettings, private val tracker: Tracker, + private val workManager: WorkManager, @TrackerLogger private val logger: FileLogger, ) : CoroutineWorker(context, workerParams) { @@ -164,7 +167,10 @@ class TrackWorker @AssistedInject constructor( } } } - }.onEach { + }.onEachIndexed { index, it -> + if (applicationContext.checkNotificationPermission()) { + notificationManager.notify(WORKER_NOTIFICATION_ID, createWorkerNotification(tracks.size, index + 1)) + } when (it) { is MangaUpdates.Failure -> { val e = it.error @@ -254,12 +260,11 @@ class TrackWorker @AssistedInject constructor( } override suspend fun getForegroundInfo(): ForegroundInfo { - val title = applicationContext.getString(R.string.check_for_new_chapters) val channel = NotificationChannelCompat.Builder( WORKER_CHANNEL_ID, - NotificationManagerCompat.IMPORTANCE_LOW + NotificationManagerCompat.IMPORTANCE_LOW, ) - .setName(title) + .setName(applicationContext.getString(R.string.check_for_new_chapters)) .setShowBadge(false) .setVibrationEnabled(false) .setSound(null, null) @@ -267,28 +272,56 @@ class TrackWorker @AssistedInject constructor( .build() notificationManager.createNotificationChannel(channel) - val notification = NotificationCompat.Builder(applicationContext, WORKER_CHANNEL_ID) - .setContentTitle(title) - .setPriority(NotificationCompat.PRIORITY_MIN) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setDefaults(0) - .setOngoing(false) - .setSilent(true) - .setProgress(0, 0, true) - .setSmallIcon(android.R.drawable.stat_notify_sync) - .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED) - .build() + val notification = createWorkerNotification(0, 0) return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { ForegroundInfo( WORKER_NOTIFICATION_ID, notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC, ) } else { ForegroundInfo(WORKER_NOTIFICATION_ID, notification) } } + private fun createWorkerNotification(max: Int, progress: Int) = NotificationCompat.Builder( + applicationContext, + WORKER_CHANNEL_ID, + ).apply { + setContentTitle(applicationContext.getString(R.string.check_for_new_chapters)) + setPriority(NotificationCompat.PRIORITY_MIN) + setCategory(NotificationCompat.CATEGORY_SERVICE) + setDefaults(0) + setOngoing(false) + setSilent(true) + setContentIntent( + PendingIntentCompat.getActivity( + applicationContext, + 0, + SettingsActivity.newTrackerSettingsIntent(applicationContext), + 0, + false, + ), + ) + addAction( + materialR.drawable.material_ic_clear_black_24dp, + applicationContext.getString(android.R.string.cancel), + workManager.createCancelPendingIntent(id), + ) + if (max > 0) { + setSubText(applicationContext.getString(R.string.fraction_pattern, progress, max)) + } + setProgress(max, progress, max == 0) + setSmallIcon(android.R.drawable.stat_notify_sync) + setForegroundServiceBehavior( + if (TAG_ONESHOT in tags) { + NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE + } else { + NotificationCompat.FOREGROUND_SERVICE_DEFERRED + }, + ) + }.build() + private suspend fun setRetryIds(ids: Set) = runInterruptible(Dispatchers.IO) { val prefs = applicationContext.getSharedPreferences(TAG, Context.MODE_PRIVATE) prefs.edit(commit = true) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cad357257..eba171f6d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -588,4 +588,5 @@ Default webtoon zoom out Fullscreen mode Hide system status and navigation bars + %1$d/%2$d