Avoid memory leak in ExceptionResolver

devel
Koitharu 6 months ago
parent 881f154b5e
commit 7a3b2a9bb4
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

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

@ -33,7 +33,6 @@ import androidx.appcompat.R as appcompatR
abstract class BaseActivity<B : ViewBinding> : abstract class BaseActivity<B : ViewBinding> :
AppCompatActivity(), AppCompatActivity(),
ExceptionResolver.Host,
OnApplyWindowInsetsListener, OnApplyWindowInsetsListener,
ScreenshotPolicyHelper.ContentContainer { ScreenshotPolicyHelper.ContentContainer {
@ -87,10 +86,6 @@ abstract class BaseActivity<B : ViewBinding> :
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR) @Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
override fun setContentView(view: View?) = throw UnsupportedOperationException() override fun setContentView(view: View?) = throw UnsupportedOperationException()
override fun getContext() = this
override fun getChildFragmentManager(): FragmentManager = supportFragmentManager
protected fun setContentView(binding: B) { protected fun setContentView(binding: B) {
this.viewBinding = binding this.viewBinding = binding
super.setContentView(binding.root) super.setContentView(binding.root)

@ -15,8 +15,7 @@ import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
abstract class BaseFragment<B : ViewBinding> : abstract class BaseFragment<B : ViewBinding> :
OnApplyWindowInsetsListener, OnApplyWindowInsetsListener,
Fragment(), Fragment() {
ExceptionResolver.Host {
var viewBinding: B? = null var viewBinding: B? = null
private set private set

@ -36,8 +36,7 @@ import com.google.android.material.R as materialR
abstract class BasePreferenceFragment(@StringRes private val titleId: Int) : abstract class BasePreferenceFragment(@StringRes private val titleId: Int) :
PreferenceFragmentCompat(), PreferenceFragmentCompat(),
OnApplyWindowInsetsListener, OnApplyWindowInsetsListener,
RecyclerViewOwner, RecyclerViewOwner {
ExceptionResolver.Host {
protected lateinit var exceptionResolver: ExceptionResolver protected lateinit var exceptionResolver: ExceptionResolver
private set private set

@ -32,8 +32,7 @@ import org.koitharu.kotatsu.core.ui.util.ActionModeDelegate
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment(), abstract class BaseAdaptiveSheet<B : ViewBinding> : AppCompatDialogFragment(),
OnApplyWindowInsetsListener, OnApplyWindowInsetsListener {
ExceptionResolver.Host {
private var waitingForDismissAllowingStateLoss = false private var waitingForDismissAllowingStateLoss = false
private var isFitToContentsDisabled = false private var isFitToContentsDisabled = false

Loading…
Cancel
Save