|
|
|
|
@@ -8,9 +8,10 @@ import androidx.collection.MutableScatterMap
|
|
|
|
|
import androidx.fragment.app.Fragment
|
|
|
|
|
import androidx.fragment.app.FragmentActivity
|
|
|
|
|
import androidx.fragment.app.FragmentManager
|
|
|
|
|
import dagger.assisted.Assisted
|
|
|
|
|
import dagger.assisted.AssistedFactory
|
|
|
|
|
import dagger.assisted.AssistedInject
|
|
|
|
|
import androidx.lifecycle.Lifecycle
|
|
|
|
|
import androidx.lifecycle.LifecycleOwner
|
|
|
|
|
import androidx.lifecycle.lifecycleScope
|
|
|
|
|
import kotlinx.coroutines.async
|
|
|
|
|
import org.koitharu.kotatsu.R
|
|
|
|
|
import org.koitharu.kotatsu.browser.BrowserActivity
|
|
|
|
|
import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
|
|
|
|
|
@@ -32,164 +33,205 @@ import org.koitharu.kotatsu.scrobbling.common.domain.ScrobblerAuthRequiredExcept
|
|
|
|
|
import org.koitharu.kotatsu.scrobbling.common.ui.ScrobblerAuthHelper
|
|
|
|
|
import org.koitharu.kotatsu.settings.sources.auth.SourceAuthActivity
|
|
|
|
|
import java.security.cert.CertPathValidatorException
|
|
|
|
|
import javax.inject.Inject
|
|
|
|
|
import javax.inject.Provider
|
|
|
|
|
import javax.net.ssl.SSLException
|
|
|
|
|
import kotlin.coroutines.Continuation
|
|
|
|
|
import kotlin.coroutines.resume
|
|
|
|
|
import kotlin.coroutines.suspendCoroutine
|
|
|
|
|
|
|
|
|
|
class ExceptionResolver @AssistedInject constructor(
|
|
|
|
|
@Assisted private val host: Host,
|
|
|
|
|
private val settings: AppSettings,
|
|
|
|
|
private val scrobblerAuthHelperProvider: Provider<ScrobblerAuthHelper>,
|
|
|
|
|
class ExceptionResolver private constructor(
|
|
|
|
|
private val host: Host,
|
|
|
|
|
private val settings: AppSettings,
|
|
|
|
|
private val scrobblerAuthHelperProvider: Provider<ScrobblerAuthHelper>,
|
|
|
|
|
) {
|
|
|
|
|
private val continuations = MutableScatterMap<String, Continuation<Boolean>>(1)
|
|
|
|
|
private val continuations = MutableScatterMap<String, Continuation<Boolean>>(1)
|
|
|
|
|
|
|
|
|
|
private val browserActionContract = host.registerForActivityResult(BrowserActivity.Contract()) {
|
|
|
|
|
handleActivityResult(BrowserActivity.TAG, true)
|
|
|
|
|
}
|
|
|
|
|
private val sourceAuthContract = host.registerForActivityResult(SourceAuthActivity.Contract()) {
|
|
|
|
|
handleActivityResult(SourceAuthActivity.TAG, it)
|
|
|
|
|
}
|
|
|
|
|
private val cloudflareContract = host.registerForActivityResult(CloudFlareActivity.Contract()) {
|
|
|
|
|
handleActivityResult(CloudFlareActivity.TAG, it)
|
|
|
|
|
}
|
|
|
|
|
private val browserActionContract = host.registerForActivityResult(BrowserActivity.Contract()) {
|
|
|
|
|
handleActivityResult(BrowserActivity.TAG, true)
|
|
|
|
|
}
|
|
|
|
|
private val sourceAuthContract = host.registerForActivityResult(SourceAuthActivity.Contract()) {
|
|
|
|
|
handleActivityResult(SourceAuthActivity.TAG, it)
|
|
|
|
|
}
|
|
|
|
|
private val cloudflareContract = host.registerForActivityResult(CloudFlareActivity.Contract()) {
|
|
|
|
|
handleActivityResult(CloudFlareActivity.TAG, it)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun showErrorDetails(e: Throwable, url: String? = null) {
|
|
|
|
|
host.router()?.showErrorDialog(e, url)
|
|
|
|
|
}
|
|
|
|
|
fun showErrorDetails(e: Throwable, url: String? = null) {
|
|
|
|
|
host.router.showErrorDialog(e, url)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
suspend fun resolve(e: Throwable): Boolean = when (e) {
|
|
|
|
|
is CloudFlareProtectedException -> resolveCF(e)
|
|
|
|
|
is AuthRequiredException -> resolveAuthException(e.source)
|
|
|
|
|
is SSLException,
|
|
|
|
|
is CertPathValidatorException -> {
|
|
|
|
|
showSslErrorDialog()
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
suspend fun resolve(e: Throwable): Boolean = host.lifecycleScope.async {
|
|
|
|
|
when (e) {
|
|
|
|
|
is CloudFlareProtectedException -> resolveCF(e)
|
|
|
|
|
is AuthRequiredException -> resolveAuthException(e.source)
|
|
|
|
|
is SSLException,
|
|
|
|
|
is CertPathValidatorException -> {
|
|
|
|
|
showSslErrorDialog()
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is InteractiveActionRequiredException -> resolveBrowserAction(e)
|
|
|
|
|
is InteractiveActionRequiredException -> resolveBrowserAction(e)
|
|
|
|
|
|
|
|
|
|
is ProxyConfigException -> {
|
|
|
|
|
host.router()?.openProxySettings()
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
is ProxyConfigException -> {
|
|
|
|
|
host.router.openProxySettings()
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is NotFoundException -> {
|
|
|
|
|
openInBrowser(e.url)
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
is NotFoundException -> {
|
|
|
|
|
openInBrowser(e.url)
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is UnsupportedSourceException -> {
|
|
|
|
|
e.manga?.let { openAlternatives(it) }
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
is UnsupportedSourceException -> {
|
|
|
|
|
e.manga?.let { openAlternatives(it) }
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is ScrobblerAuthRequiredException -> {
|
|
|
|
|
val authHelper = scrobblerAuthHelperProvider.get()
|
|
|
|
|
if (authHelper.isAuthorized(e.scrobbler)) {
|
|
|
|
|
true
|
|
|
|
|
} else {
|
|
|
|
|
host.withContext {
|
|
|
|
|
authHelper.startAuth(this, e.scrobbler).onFailure(::showErrorDetails)
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
is ScrobblerAuthRequiredException -> {
|
|
|
|
|
val authHelper = scrobblerAuthHelperProvider.get()
|
|
|
|
|
if (authHelper.isAuthorized(e.scrobbler)) {
|
|
|
|
|
true
|
|
|
|
|
} else {
|
|
|
|
|
host.withContext {
|
|
|
|
|
authHelper.startAuth(this, e.scrobbler).onFailure(::showErrorDetails)
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
else -> false
|
|
|
|
|
}
|
|
|
|
|
}.await()
|
|
|
|
|
|
|
|
|
|
private suspend fun resolveBrowserAction(
|
|
|
|
|
e: InteractiveActionRequiredException
|
|
|
|
|
): Boolean = suspendCoroutine { cont ->
|
|
|
|
|
continuations[BrowserActivity.TAG] = cont
|
|
|
|
|
browserActionContract.launch(e)
|
|
|
|
|
}
|
|
|
|
|
private suspend fun resolveBrowserAction(
|
|
|
|
|
e: InteractiveActionRequiredException
|
|
|
|
|
): Boolean = suspendCoroutine { cont ->
|
|
|
|
|
continuations[BrowserActivity.TAG] = cont
|
|
|
|
|
browserActionContract.launch(e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun resolveCF(e: CloudFlareProtectedException): Boolean = suspendCoroutine { cont ->
|
|
|
|
|
continuations[CloudFlareActivity.TAG] = cont
|
|
|
|
|
cloudflareContract.launch(e)
|
|
|
|
|
}
|
|
|
|
|
private suspend fun resolveCF(e: CloudFlareProtectedException): Boolean = suspendCoroutine { cont ->
|
|
|
|
|
continuations[CloudFlareActivity.TAG] = cont
|
|
|
|
|
cloudflareContract.launch(e)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
|
|
|
|
|
continuations[SourceAuthActivity.TAG] = cont
|
|
|
|
|
sourceAuthContract.launch(source)
|
|
|
|
|
}
|
|
|
|
|
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
|
|
|
|
|
continuations[SourceAuthActivity.TAG] = cont
|
|
|
|
|
sourceAuthContract.launch(source)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun openInBrowser(url: String) {
|
|
|
|
|
host.router()?.openBrowser(url, null, null)
|
|
|
|
|
}
|
|
|
|
|
private fun openInBrowser(url: String) {
|
|
|
|
|
host.router.openBrowser(url, null, null)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun openAlternatives(manga: Manga) {
|
|
|
|
|
host.router()?.openAlternatives(manga)
|
|
|
|
|
}
|
|
|
|
|
private fun openAlternatives(manga: Manga) {
|
|
|
|
|
host.router.openAlternatives(manga)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun handleActivityResult(tag: String, result: Boolean) {
|
|
|
|
|
continuations.remove(tag)?.resume(result)
|
|
|
|
|
}
|
|
|
|
|
private fun handleActivityResult(tag: String, result: Boolean) {
|
|
|
|
|
continuations.remove(tag)?.resume(result)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun showSslErrorDialog() {
|
|
|
|
|
val ctx = host.getContext() ?: return
|
|
|
|
|
if (settings.isSSLBypassEnabled) {
|
|
|
|
|
Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
buildAlertDialog(ctx) {
|
|
|
|
|
setTitle(R.string.ignore_ssl_errors)
|
|
|
|
|
setMessage(R.string.ignore_ssl_errors_summary)
|
|
|
|
|
setPositiveButton(R.string.apply) { _, _ ->
|
|
|
|
|
settings.isSSLBypassEnabled = true
|
|
|
|
|
Toast.makeText(ctx, R.string.settings_apply_restart_required, Toast.LENGTH_LONG).show()
|
|
|
|
|
ctx.restartApplication()
|
|
|
|
|
}
|
|
|
|
|
setNegativeButton(android.R.string.cancel, null)
|
|
|
|
|
}.show()
|
|
|
|
|
}
|
|
|
|
|
private fun showSslErrorDialog() {
|
|
|
|
|
val ctx = host.context ?: return
|
|
|
|
|
if (settings.isSSLBypassEnabled) {
|
|
|
|
|
Toast.makeText(ctx, R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
buildAlertDialog(ctx) {
|
|
|
|
|
setTitle(R.string.ignore_ssl_errors)
|
|
|
|
|
setMessage(R.string.ignore_ssl_errors_summary)
|
|
|
|
|
setPositiveButton(R.string.apply) { _, _ ->
|
|
|
|
|
settings.isSSLBypassEnabled = true
|
|
|
|
|
Toast.makeText(ctx, R.string.settings_apply_restart_required, Toast.LENGTH_LONG).show()
|
|
|
|
|
ctx.restartApplication()
|
|
|
|
|
}
|
|
|
|
|
setNegativeButton(android.R.string.cancel, null)
|
|
|
|
|
}.show()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private inline fun Host.withContext(block: Context.() -> Unit) {
|
|
|
|
|
getContext()?.apply(block)
|
|
|
|
|
}
|
|
|
|
|
class Factory @Inject constructor(
|
|
|
|
|
private val settings: AppSettings,
|
|
|
|
|
private val scrobblerAuthHelperProvider: Provider<ScrobblerAuthHelper>,
|
|
|
|
|
) {
|
|
|
|
|
|
|
|
|
|
private fun Host.router(): AppRouter? = when (this) {
|
|
|
|
|
is FragmentActivity -> router
|
|
|
|
|
is Fragment -> router
|
|
|
|
|
else -> null
|
|
|
|
|
}
|
|
|
|
|
fun create(fragment: Fragment) = ExceptionResolver(
|
|
|
|
|
host = Host.FragmentHost(fragment),
|
|
|
|
|
settings = settings,
|
|
|
|
|
scrobblerAuthHelperProvider = scrobblerAuthHelperProvider,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
interface Host : ActivityResultCaller {
|
|
|
|
|
fun create(activity: FragmentActivity) = ExceptionResolver(
|
|
|
|
|
host = Host.ActivityHost(activity),
|
|
|
|
|
settings = settings,
|
|
|
|
|
scrobblerAuthHelperProvider = scrobblerAuthHelperProvider,
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun getChildFragmentManager(): FragmentManager
|
|
|
|
|
private sealed interface Host : ActivityResultCaller, LifecycleOwner {
|
|
|
|
|
|
|
|
|
|
fun getContext(): Context?
|
|
|
|
|
}
|
|
|
|
|
val context: Context?
|
|
|
|
|
|
|
|
|
|
@AssistedFactory
|
|
|
|
|
interface Factory {
|
|
|
|
|
val router: AppRouter
|
|
|
|
|
|
|
|
|
|
fun create(host: Host): ExceptionResolver
|
|
|
|
|
}
|
|
|
|
|
val fragmentManager: FragmentManager
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
inline fun withContext(block: Context.() -> Unit) {
|
|
|
|
|
context?.apply(block)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@StringRes
|
|
|
|
|
fun getResolveStringId(e: Throwable) = when (e) {
|
|
|
|
|
is CloudFlareProtectedException -> R.string.captcha_solve
|
|
|
|
|
is ScrobblerAuthRequiredException,
|
|
|
|
|
is AuthRequiredException -> R.string.sign_in
|
|
|
|
|
class ActivityHost(val activity: FragmentActivity) : Host,
|
|
|
|
|
ActivityResultCaller by activity,
|
|
|
|
|
LifecycleOwner by activity {
|
|
|
|
|
|
|
|
|
|
is NotFoundException -> if (e.url.isHttpUrl()) R.string.open_in_browser else 0
|
|
|
|
|
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
|
|
|
|
|
is SSLException,
|
|
|
|
|
is CertPathValidatorException -> R.string.fix
|
|
|
|
|
override val context: Context
|
|
|
|
|
get() = activity
|
|
|
|
|
|
|
|
|
|
is ProxyConfigException -> R.string.settings
|
|
|
|
|
override val router: AppRouter
|
|
|
|
|
get() = activity.router
|
|
|
|
|
|
|
|
|
|
is InteractiveActionRequiredException -> R.string._continue
|
|
|
|
|
override val fragmentManager: FragmentManager
|
|
|
|
|
get() = activity.supportFragmentManager
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> 0
|
|
|
|
|
}
|
|
|
|
|
class FragmentHost(val fragment: Fragment) : Host,
|
|
|
|
|
ActivityResultCaller by fragment {
|
|
|
|
|
|
|
|
|
|
fun canResolve(e: Throwable) = getResolveStringId(e) != 0
|
|
|
|
|
}
|
|
|
|
|
override val context: Context?
|
|
|
|
|
get() = fragment.context
|
|
|
|
|
|
|
|
|
|
override val router: AppRouter
|
|
|
|
|
get() = fragment.router
|
|
|
|
|
|
|
|
|
|
override val fragmentManager: FragmentManager
|
|
|
|
|
get() = fragment.childFragmentManager
|
|
|
|
|
|
|
|
|
|
override val lifecycle: Lifecycle
|
|
|
|
|
get() = fragment.viewLifecycleOwner.lifecycle
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
|
|
|
|
@StringRes
|
|
|
|
|
fun getResolveStringId(e: Throwable) = when (e) {
|
|
|
|
|
is CloudFlareProtectedException -> R.string.captcha_solve
|
|
|
|
|
is ScrobblerAuthRequiredException,
|
|
|
|
|
is AuthRequiredException -> R.string.sign_in
|
|
|
|
|
|
|
|
|
|
is NotFoundException -> if (e.url.isHttpUrl()) R.string.open_in_browser else 0
|
|
|
|
|
is UnsupportedSourceException -> if (e.manga != null) R.string.alternatives else 0
|
|
|
|
|
is SSLException,
|
|
|
|
|
is CertPathValidatorException -> R.string.fix
|
|
|
|
|
|
|
|
|
|
is ProxyConfigException -> R.string.settings
|
|
|
|
|
|
|
|
|
|
is InteractiveActionRequiredException -> R.string._continue
|
|
|
|
|
|
|
|
|
|
else -> 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun canResolve(e: Throwable) = getResolveStringId(e) != 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|