diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt index ff00392d1..0c711b879 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -135,7 +135,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) { get() = prefs.getEnumValue(KEY_ZOOM_MODE, ZoomMode.FIT_CENTER) val trackSources: Set - get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: arraySetOf(TRACK_FAVOURITES, TRACK_HISTORY) + get() = prefs.getStringSet(KEY_TRACK_SOURCES, null) ?: setOf(TRACK_FAVOURITES) var appPassword: String? get() = prefs.getString(KEY_APP_PASSWORD, null) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt index 0bb946be3..42cfcaab0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt @@ -1,5 +1,6 @@ package org.koitharu.kotatsu.core.util.ext +import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.app.ActivityManager @@ -13,6 +14,7 @@ import android.content.ContextWrapper import android.content.OperationApplicationException import android.content.SharedPreferences import android.content.SyncResult +import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.database.SQLException import android.graphics.Color @@ -28,6 +30,8 @@ import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.annotation.IntegerRes import androidx.core.app.ActivityOptionsCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.content.ContextCompat import androidx.core.os.LocaleListCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope @@ -220,3 +224,9 @@ inline fun Activity.catchingWebViewUnavailability(block: () -> Unit): Boolean { } } } + +fun Context.checkNotificationPermission(): Boolean = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED +} else { + NotificationManagerCompat.from(this).areNotificationsEnabled() +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt index fcc8f996a..7e5643d4e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/tracker/domain/Tracker.kt @@ -25,7 +25,7 @@ class Tracker @Inject constructor( if (sources.isEmpty()) { return emptyList() } - val knownIds = HashSet() + val knownManga = HashSet() val result = ArrayList() // Favourites if (AppSettings.TRACK_FAVOURITES in sources) { @@ -42,7 +42,7 @@ class Tracker @Inject constructor( null } for (track in categoryTracks) { - if (knownIds.add(track.manga)) { + if (knownManga.add(track.manga.id)) { result.add(TrackingItem(track, channelId)) } } @@ -58,7 +58,7 @@ class Tracker @Inject constructor( null } for (track in historyTracks) { - if (knownIds.add(track.manga)) { + if (knownManga.add(track.manga.id)) { result.add(TrackingItem(track, channelId)) } } 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 7cd1f04ec..660ee1dc1 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 @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.tracker.work -import android.annotation.SuppressLint import android.app.PendingIntent import android.content.Context import android.os.Build @@ -34,11 +33,11 @@ import dagger.Reusable import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.NonCancellable -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext @@ -49,6 +48,7 @@ import org.koitharu.kotatsu.core.logs.FileLogger 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.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.trySetForeground import org.koitharu.kotatsu.details.ui.DetailsActivity @@ -70,6 +70,7 @@ class TrackWorker @AssistedInject constructor( private val tracker: Tracker, @TrackerLogger private val logger: FileLogger, ) : CoroutineWorker(context, workerParams) { + private val notificationManager by lazy { NotificationManagerCompat.from(applicationContext) } override suspend fun doWork(): Result { @@ -121,35 +122,36 @@ class TrackWorker @AssistedInject constructor( private suspend fun checkUpdatesAsync(tracks: List): List { val semaphore = Semaphore(MAX_PARALLELISM) - return supervisorScope { - tracks.map { (track, channelId) -> - async { + return channelFlow { + for ((track, channelId) in tracks) { + launch { semaphore.withPermit { - runCatchingCancellable { - tracker.fetchUpdates(track, commit = true) - }.onFailure { e -> - if (e is CloudFlareProtectedException) { - CaptchaNotifier(applicationContext).notify(e) - } - logger.log("checkUpdatesAsync", e) - }.onSuccess { updates -> - if (updates.isValid && updates.isNotEmpty()) { - showNotification( - manga = updates.manga, - channelId = channelId, - newChapters = updates.newChapters, - ) - } - }.getOrNull() + send( + runCatchingCancellable { + tracker.fetchUpdates(track, commit = true) + }.onFailure { e -> + if (e is CloudFlareProtectedException) { + CaptchaNotifier(applicationContext).notify(e) + } + logger.log("checkUpdatesAsync", e) + }.onSuccess { updates -> + if (updates.isValid && updates.isNotEmpty()) { + showNotification( + manga = updates.manga, + channelId = channelId, + newChapters = updates.newChapters, + ) + } + }.getOrNull(), + ) } } - }.awaitAll() - } + } + }.toList(ArrayList(tracks.size)) } - @SuppressLint("MissingPermission") private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List) { - if (newChapters.isEmpty() || channelId == null || !notificationManager.areNotificationsEnabled()) { + if (newChapters.isEmpty() || channelId == null || !applicationContext.checkNotificationPermission()) { return } val id = manga.url.hashCode()