Show notification if captcha required in background
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@ package org.koitharu.kotatsu.core.exceptions
|
|||||||
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
class CloudFlareProtectedException(
|
class CloudFlareProtectedException(
|
||||||
val url: String,
|
val url: String,
|
||||||
|
val source: MangaSource?,
|
||||||
@Transient val headers: Headers,
|
@Transient val headers: Headers,
|
||||||
) : IOException("Protected by CloudFlare")
|
) : IOException("Protected by CloudFlare")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import okhttp3.Interceptor
|
|||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
|
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_FORBIDDEN
|
||||||
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
|
import java.net.HttpURLConnection.HTTP_UNAVAILABLE
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ class CloudFlareInterceptor : Interceptor {
|
|||||||
response.closeQuietly()
|
response.closeQuietly()
|
||||||
throw CloudFlareProtectedException(
|
throw CloudFlareProtectedException(
|
||||||
url = request.url.toString(),
|
url = request.url.toString(),
|
||||||
|
source = request.tag(MangaSource::class.java),
|
||||||
headers = request.headers,
|
headers = request.headers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import org.koitharu.kotatsu.R
|
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.model.distinctById
|
||||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
@@ -80,6 +82,7 @@ class SuggestionsWorker @AssistedInject constructor(
|
|||||||
private val appSettings: AppSettings,
|
private val appSettings: AppSettings,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
private val sourcesRepository: MangaSourcesRepository,
|
private val sourcesRepository: MangaSourcesRepository,
|
||||||
|
private val captchaNotifier: CaptchaNotifier,
|
||||||
) : CoroutineWorker(appContext, params) {
|
) : CoroutineWorker(appContext, params) {
|
||||||
|
|
||||||
private val notificationManager by lazy { NotificationManagerCompat.from(appContext) }
|
private val notificationManager by lazy { NotificationManagerCompat.from(appContext) }
|
||||||
@@ -206,11 +209,17 @@ class SuggestionsWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
list.shuffle()
|
list.shuffle()
|
||||||
list.take(MAX_SOURCE_RESULTS)
|
list.take(MAX_SOURCE_RESULTS)
|
||||||
}.onFailure {
|
}.onFailure { e ->
|
||||||
it.printStackTraceDebug()
|
if (e is CloudFlareProtectedException) {
|
||||||
|
captchaNotifier.notify(e)
|
||||||
|
}
|
||||||
|
e.printStackTraceDebug()
|
||||||
}.getOrDefault(emptyList())
|
}.getOrDefault(emptyList())
|
||||||
|
|
||||||
private suspend fun showNotification(manga: Manga) {
|
private suspend fun showNotification(manga: Manga) {
|
||||||
|
if (!notificationManager.areNotificationsEnabled()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
val channel = NotificationChannelCompat.Builder(MANGA_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
val channel = NotificationChannelCompat.Builder(MANGA_CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||||
.setName(applicationContext.getString(R.string.suggestions))
|
.setName(applicationContext.getString(R.string.suggestions))
|
||||||
.setDescription(applicationContext.getString(R.string.suggestions_summary))
|
.setDescription(applicationContext.getString(R.string.suggestions_summary))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.koitharu.kotatsu.tracker.work
|
package org.koitharu.kotatsu.tracker.work
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -42,6 +43,8 @@ import kotlinx.coroutines.sync.Semaphore
|
|||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.R
|
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.FileLogger
|
||||||
import org.koitharu.kotatsu.core.logs.TrackerLogger
|
import org.koitharu.kotatsu.core.logs.TrackerLogger
|
||||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||||
@@ -66,6 +69,7 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
private val settings: AppSettings,
|
private val settings: AppSettings,
|
||||||
private val tracker: Tracker,
|
private val tracker: Tracker,
|
||||||
@TrackerLogger private val logger: FileLogger,
|
@TrackerLogger private val logger: FileLogger,
|
||||||
|
private val captchaNotifier: CaptchaNotifier,
|
||||||
) : CoroutineWorker(context, workerParams) {
|
) : CoroutineWorker(context, workerParams) {
|
||||||
private val notificationManager by lazy { NotificationManagerCompat.from(applicationContext) }
|
private val notificationManager by lazy { NotificationManagerCompat.from(applicationContext) }
|
||||||
|
|
||||||
@@ -124,8 +128,11 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
semaphore.withPermit {
|
semaphore.withPermit {
|
||||||
runCatchingCancellable {
|
runCatchingCancellable {
|
||||||
tracker.fetchUpdates(track, commit = true)
|
tracker.fetchUpdates(track, commit = true)
|
||||||
}.onFailure {
|
}.onFailure { e ->
|
||||||
logger.log("checkUpdatesAsync", it)
|
if (e is CloudFlareProtectedException) {
|
||||||
|
captchaNotifier.notify(e)
|
||||||
|
}
|
||||||
|
logger.log("checkUpdatesAsync", e)
|
||||||
}.onSuccess { updates ->
|
}.onSuccess { updates ->
|
||||||
if (updates.isValid && updates.isNotEmpty()) {
|
if (updates.isValid && updates.isNotEmpty()) {
|
||||||
showNotification(
|
showNotification(
|
||||||
@@ -141,8 +148,9 @@ class TrackWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List<MangaChapter>) {
|
private suspend fun showNotification(manga: Manga, channelId: String?, newChapters: List<MangaChapter>) {
|
||||||
if (newChapters.isEmpty() || channelId == null) {
|
if (newChapters.isEmpty() || channelId == null || !notificationManager.areNotificationsEnabled()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val id = manga.url.hashCode()
|
val id = manga.url.hashCode()
|
||||||
|
|||||||
@@ -468,4 +468,5 @@
|
|||||||
<string name="order_added">Added</string>
|
<string name="order_added">Added</string>
|
||||||
<string name="view_list">View list</string>
|
<string name="view_list">View list</string>
|
||||||
<string name="show">Show</string>
|
<string name="show">Show</string>
|
||||||
|
<string name="captcha_required_summary">%s requires a captcha to be resolved to work properly</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user