diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt index b8720c1d4..449c4a06c 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/exceptions/resolve/ExceptionResolver.kt @@ -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, +class ExceptionResolver private constructor( + private val host: Host, + private val settings: AppSettings, + private val scrobblerAuthHelperProvider: Provider, ) { - private val continuations = MutableScatterMap>(1) + private val continuations = MutableScatterMap>(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, + ) { - 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 + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt index 6b2ade41f..21d862ffe 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseActivity.kt @@ -33,7 +33,6 @@ import androidx.appcompat.R as appcompatR abstract class BaseActivity : AppCompatActivity(), - ExceptionResolver.Host, OnApplyWindowInsetsListener, ScreenshotPolicyHelper.ContentContainer { @@ -87,10 +86,6 @@ abstract class BaseActivity : @Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR) override fun setContentView(view: View?) = throw UnsupportedOperationException() - override fun getContext() = this - - override fun getChildFragmentManager(): FragmentManager = supportFragmentManager - protected fun setContentView(binding: B) { this.viewBinding = binding super.setContentView(binding.root) diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt index a275062dc..80ad9108d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BaseFragment.kt @@ -15,8 +15,7 @@ import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate abstract class BaseFragment : OnApplyWindowInsetsListener, - Fragment(), - ExceptionResolver.Host { + Fragment() { var viewBinding: B? = null private set diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt index af0ae7ff1..683e62c24 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/BasePreferenceFragment.kt @@ -36,8 +36,7 @@ import com.google.android.material.R as materialR abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : PreferenceFragmentCompat(), OnApplyWindowInsetsListener, - RecyclerViewOwner, - ExceptionResolver.Host { + RecyclerViewOwner { protected lateinit var exceptionResolver: ExceptionResolver private set diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt index 663aa0ffa..9732c138d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/sheet/BaseAdaptiveSheet.kt @@ -32,8 +32,7 @@ import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate import com.google.android.material.R as materialR abstract class BaseAdaptiveSheet : AppCompatDialogFragment(), - OnApplyWindowInsetsListener, - ExceptionResolver.Host { + OnApplyWindowInsetsListener { private var waitingForDismissAllowingStateLoss = false private var isFitToContentsDisabled = false