Show notification if captcha required in background

This commit is contained in:
Koitharu
2023-07-28 16:09:46 +03:00
parent 0f7bceb268
commit 3e48ce85fd
6 changed files with 98 additions and 5 deletions

View File

@@ -0,0 +1,71 @@
package org.koitharu.kotatsu.browser.cloudflare
import android.annotation.SuppressLint
import android.content.Context
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.PendingIntentCompat
import androidx.core.net.toUri
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.parsers.model.ContentType
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class CaptchaNotifier @Inject constructor(
@ApplicationContext private val context: Context,
) {
private val mutex = Mutex()
@SuppressLint("MissingPermission")
suspend fun notify(exception: CloudFlareProtectedException) = mutex.withLock {
val manager = NotificationManagerCompat.from(context)
if (!manager.areNotificationsEnabled()) {
return@withLock
}
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(context.getString(R.string.captcha_required))
.setShowBadge(true)
.setVibrationEnabled(false)
.setSound(null, null)
.setLightsEnabled(false)
.build()
manager.createNotificationChannel(channel)
val intent = CloudFlareActivity.newIntent(context, exception.url, exception.headers)
.setData(exception.url.toUri())
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(channel.name)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(NotificationCompat.DEFAULT_SOUND)
.setSmallIcon(android.R.drawable.stat_notify_error)
.setVisibility(
if (exception.source?.contentType == ContentType.HENTAI) {
NotificationCompat.VISIBILITY_SECRET
} else {
NotificationCompat.VISIBILITY_PUBLIC
},
)
.setContentText(
context.getString(
R.string.captcha_required_summary,
exception.source?.title ?: context.getString(R.string.app_name),
),
)
.setContentIntent(PendingIntentCompat.getActivity(context, 0, intent, 0, false))
.build()
manager.notify(TAG, exception.source.hashCode(), notification)
}
private companion object {
private const val CHANNEL_ID = "captcha"
private const val TAG = CHANNEL_ID
}
}

View File

@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.core.exceptions
import okhttp3.Headers
import okio.IOException
import org.koitharu.kotatsu.parsers.model.MangaSource
class CloudFlareProtectedException(
val url: String,
val source: MangaSource?,
@Transient val headers: Headers,
) : IOException("Protected by CloudFlare")

View File

@@ -4,6 +4,7 @@ import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.internal.closeQuietly
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.parsers.model.MangaSource
import java.net.HttpURLConnection.HTTP_FORBIDDEN
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
@@ -20,6 +21,7 @@ class CloudFlareInterceptor : Interceptor {
response.closeQuietly()
throw CloudFlareProtectedException(
url = request.url.toString(),
source = request.tag(MangaSource::class.java),
headers = request.headers,
)
}

View File

@@ -38,6 +38,8 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.model.distinctById
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -80,6 +82,7 @@ class SuggestionsWorker @AssistedInject constructor(
private val appSettings: AppSettings,
private val mangaRepositoryFactory: MangaRepository.Factory,
private val sourcesRepository: MangaSourcesRepository,
private val captchaNotifier: CaptchaNotifier,
) : CoroutineWorker(appContext, params) {
private val notificationManager by lazy { NotificationManagerCompat.from(appContext) }
@@ -206,11 +209,17 @@ class SuggestionsWorker @AssistedInject constructor(
}
list.shuffle()
list.take(MAX_SOURCE_RESULTS)
}.onFailure {
it.printStackTraceDebug()
}.onFailure { e ->
if (e is CloudFlareProtectedException) {
captchaNotifier.notify(e)
}
e.printStackTraceDebug()
}.getOrDefault(emptyList())
private suspend fun showNotification(manga: Manga) {
if (!notificationManager.areNotificationsEnabled()) {
return
}
val channel = NotificationChannelCompat.Builder(MANGA_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
.setName(applicationContext.getString(R.string.suggestions))
.setDescription(applicationContext.getString(R.string.suggestions_summary))

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.tracker.work
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.os.Build
@@ -42,6 +43,8 @@ import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.cloudflare.CaptchaNotifier
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.logs.FileLogger
import org.koitharu.kotatsu.core.logs.TrackerLogger
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -66,6 +69,7 @@ class TrackWorker @AssistedInject constructor(
private val settings: AppSettings,
private val tracker: Tracker,
@TrackerLogger private val logger: FileLogger,
private val captchaNotifier: CaptchaNotifier,
) : CoroutineWorker(context, workerParams) {
private val notificationManager by lazy { NotificationManagerCompat.from(applicationContext) }
@@ -124,8 +128,11 @@ class TrackWorker @AssistedInject constructor(
semaphore.withPermit {
runCatchingCancellable {
tracker.fetchUpdates(track, commit = true)
}.onFailure {
logger.log("checkUpdatesAsync", it)
}.onFailure { e ->
if (e is CloudFlareProtectedException) {
captchaNotifier.notify(e)
}
logger.log("checkUpdatesAsync", e)
}.onSuccess { updates ->
if (updates.isValid && updates.isNotEmpty()) {
showNotification(
@@ -141,8 +148,9 @@ class TrackWorker @AssistedInject constructor(
}
}
@SuppressLint("MissingPermission")
private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List<MangaChapter>) {
if (newChapters.isEmpty() || channelId == null) {
if (newChapters.isEmpty() || channelId == null || !notificationManager.areNotificationsEnabled()) {
return
}
val id = manga.url.hashCode()

View File

@@ -468,4 +468,5 @@
<string name="order_added">Added</string>
<string name="view_list">View list</string>
<string name="show">Show</string>
<string name="captcha_required_summary">%s requires a captcha to be resolved to work properly</string>
</resources>