diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 10faa7ae2..2a167d459 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -102,6 +102,10 @@
android:name="org.koitharu.kotatsu.browser.BrowserActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" />
+
(), BrowserCallback
override fun onDestroy() {
super.onDestroy()
+ viewBinding.webView.stopLoading()
viewBinding.webView.destroy()
}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt
new file mode 100644
index 000000000..51d29fc9f
--- /dev/null
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt
@@ -0,0 +1,175 @@
+package org.koitharu.kotatsu.browser.cloudflare
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.MenuItem
+import android.webkit.CookieManager
+import android.webkit.WebSettings
+import androidx.activity.result.contract.ActivityResultContract
+import androidx.core.graphics.Insets
+import androidx.core.net.toUri
+import androidx.core.view.isInvisible
+import androidx.core.view.isVisible
+import androidx.core.view.updatePadding
+import dagger.hilt.android.AndroidEntryPoint
+import okhttp3.Headers
+import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
+import org.koitharu.kotatsu.R
+import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
+import org.koitharu.kotatsu.core.network.CommonHeaders
+import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
+import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
+import org.koitharu.kotatsu.core.ui.BaseActivity
+import org.koitharu.kotatsu.core.util.TaggedActivityResult
+import org.koitharu.kotatsu.core.util.ext.catchingWebViewUnavailability
+import org.koitharu.kotatsu.databinding.ActivityBrowserBinding
+import javax.inject.Inject
+import com.google.android.material.R as materialR
+
+@AndroidEntryPoint
+class CloudFlareActivity : BaseActivity(), CloudFlareCallback {
+
+ private var pendingResult = RESULT_CANCELED
+
+ @Inject
+ lateinit var cookieJar: MutableCookieJar
+
+ private var onBackPressedCallback: WebViewBackPressedCallback? = null
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (!catchingWebViewUnavailability { setContentView(ActivityBrowserBinding.inflate(layoutInflater)) }) {
+ return
+ }
+ supportActionBar?.run {
+ setDisplayHomeAsUpEnabled(true)
+ setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material)
+ }
+ val url = intent?.dataString.orEmpty()
+ with(viewBinding.webView.settings) {
+ javaScriptEnabled = true
+ cacheMode = WebSettings.LOAD_DEFAULT
+ domStorageEnabled = true
+ databaseEnabled = true
+ userAgentString = intent?.getStringExtra(ARG_UA) ?: CommonHeadersInterceptor.userAgentFallback
+ }
+ viewBinding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
+ onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also {
+ onBackPressedDispatcher.addCallback(it)
+ }
+ CookieManager.getInstance().setAcceptThirdPartyCookies(viewBinding.webView, true)
+ if (savedInstanceState != null) {
+ return
+ }
+ if (url.isEmpty()) {
+ finishAfterTransition()
+ } else {
+ onTitleChanged(getString(R.string.loading_), url)
+ viewBinding.webView.loadUrl(url)
+ }
+ }
+
+ override fun onDestroy() {
+ viewBinding.webView.run {
+ stopLoading()
+ destroy()
+ }
+ super.onDestroy()
+ }
+
+ override fun onSaveInstanceState(outState: Bundle) {
+ super.onSaveInstanceState(outState)
+ viewBinding.webView.saveState(outState)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+ viewBinding.webView.restoreState(savedInstanceState)
+ }
+
+ override fun onWindowInsetsChanged(insets: Insets) {
+ viewBinding.appbar.updatePadding(
+ top = insets.top,
+ )
+ viewBinding.root.updatePadding(
+ left = insets.left,
+ right = insets.right,
+ bottom = insets.bottom,
+ )
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
+ android.R.id.home -> {
+ viewBinding.webView.stopLoading()
+ finishAfterTransition()
+ true
+ }
+
+ else -> super.onOptionsItemSelected(item)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ viewBinding.webView.onResume()
+ }
+
+ override fun onPause() {
+ viewBinding.webView.onPause()
+ super.onPause()
+ }
+
+ override fun finish() {
+ setResult(pendingResult)
+ super.finish()
+ }
+
+ override fun onPageLoaded() {
+ viewBinding.progressBar.isInvisible = true
+ }
+
+ override fun onCheckPassed() {
+ pendingResult = RESULT_OK
+ finishAfterTransition()
+ }
+
+ override fun onLoadingStateChanged(isLoading: Boolean) {
+ viewBinding.progressBar.isVisible = isLoading
+ }
+
+ override fun onHistoryChanged() {
+ onBackPressedCallback?.onHistoryChanged()
+ }
+
+ override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) {
+ setTitle(title)
+ supportActionBar?.subtitle = subtitle?.toString()?.toHttpUrlOrNull()?.topPrivateDomain() ?: subtitle
+ }
+
+ class Contract : ActivityResultContract, TaggedActivityResult>() {
+ override fun createIntent(context: Context, input: Pair): Intent {
+ return newIntent(context, input.first, input.second)
+ }
+
+ override fun parseResult(resultCode: Int, intent: Intent?): TaggedActivityResult {
+ return TaggedActivityResult(TAG, resultCode)
+ }
+ }
+
+ companion object {
+
+ const val TAG = "CloudFlareActivity"
+ private const val ARG_UA = "ua"
+
+ fun newIntent(
+ context: Context,
+ url: String,
+ headers: Headers?,
+ ) = Intent(context, CloudFlareActivity::class.java).apply {
+ data = url.toUri()
+ headers?.get(CommonHeaders.USER_AGENT)?.let {
+ putExtra(ARG_UA, it)
+ }
+ }
+ }
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt
deleted file mode 100644
index 0a1284448..000000000
--- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareDialog.kt
+++ /dev/null
@@ -1,123 +0,0 @@
-package org.koitharu.kotatsu.browser.cloudflare
-
-import android.content.DialogInterface
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import android.webkit.CookieManager
-import android.webkit.WebSettings
-import androidx.appcompat.app.AlertDialog
-import androidx.core.view.isInvisible
-import androidx.fragment.app.setFragmentResult
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import dagger.hilt.android.AndroidEntryPoint
-import okhttp3.Headers
-import org.koitharu.kotatsu.browser.WebViewBackPressedCallback
-import org.koitharu.kotatsu.core.network.CommonHeaders
-import org.koitharu.kotatsu.core.network.CommonHeadersInterceptor
-import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar
-import org.koitharu.kotatsu.core.ui.AlertDialogFragment
-import org.koitharu.kotatsu.core.util.ext.withArgs
-import org.koitharu.kotatsu.databinding.FragmentCloudflareBinding
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class CloudFlareDialog : AlertDialogFragment(), CloudFlareCallback {
-
- private lateinit var url: String
- private val pendingResult = Bundle(1)
-
- @Inject
- lateinit var cookieJar: MutableCookieJar
-
- private var onBackPressedCallback: WebViewBackPressedCallback? = null
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- url = requireArguments().getString(ARG_URL).orEmpty()
- }
-
- override fun onCreateViewBinding(
- inflater: LayoutInflater,
- container: ViewGroup?,
- ) = FragmentCloudflareBinding.inflate(inflater, container, false)
-
- override fun onViewBindingCreated(binding: FragmentCloudflareBinding, savedInstanceState: Bundle?) {
- super.onViewBindingCreated(binding, savedInstanceState)
- with(binding.webView.settings) {
- javaScriptEnabled = true
- cacheMode = WebSettings.LOAD_DEFAULT
- domStorageEnabled = true
- databaseEnabled = true
- userAgentString = arguments?.getString(ARG_UA) ?: CommonHeadersInterceptor.userAgentChrome
- }
- binding.webView.webViewClient = CloudFlareClient(cookieJar, this, url)
- CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
- if (url.isEmpty()) {
- dismissAllowingStateLoss()
- } else {
- binding.webView.loadUrl(url)
- }
- }
-
- override fun onDestroyView() {
- requireViewBinding().webView.stopLoading()
- requireViewBinding().webView.destroy()
- onBackPressedCallback = null
- super.onDestroyView()
- }
-
- override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
- return super.onBuildDialog(builder).setNegativeButton(android.R.string.cancel, null)
- }
-
- override fun onDialogCreated(dialog: AlertDialog) {
- super.onDialogCreated(dialog)
- onBackPressedCallback = WebViewBackPressedCallback(requireViewBinding().webView).also {
- dialog.onBackPressedDispatcher.addCallback(it)
- }
- }
-
- override fun onResume() {
- super.onResume()
- requireViewBinding().webView.onResume()
- }
-
- override fun onPause() {
- requireViewBinding().webView.onPause()
- super.onPause()
- }
-
- override fun onDismiss(dialog: DialogInterface) {
- setFragmentResult(TAG, pendingResult)
- super.onDismiss(dialog)
- }
-
- override fun onPageLoaded() {
- viewBinding?.progressBar?.isInvisible = true
- }
-
- override fun onCheckPassed() {
- pendingResult.putBoolean(EXTRA_RESULT, true)
- dismissAllowingStateLoss()
- }
-
- override fun onHistoryChanged() {
- onBackPressedCallback?.onHistoryChanged()
- }
-
- companion object {
-
- const val TAG = "CloudFlareDialog"
- const val EXTRA_RESULT = "result"
- private const val ARG_URL = "url"
- private const val ARG_UA = "ua"
-
- fun newInstance(url: String, headers: Headers?) = CloudFlareDialog().withArgs(2) {
- putString(ARG_URL, url)
- headers?.get(CommonHeaders.USER_AGENT)?.let {
- putString(ARG_UA, it)
- }
- }
- }
-}
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 b5e267dcb..9407ab6e9 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
@@ -6,15 +6,13 @@ import androidx.annotation.StringRes
import androidx.collection.ArrayMap
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
-import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Headers
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.browser.BrowserActivity
-import org.koitharu.kotatsu.browser.cloudflare.CloudFlareDialog
+import org.koitharu.kotatsu.browser.cloudflare.CloudFlareActivity
import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException
import org.koitharu.kotatsu.core.ui.dialog.ErrorDetailsDialog
import org.koitharu.kotatsu.core.util.TaggedActivityResult
-import org.koitharu.kotatsu.core.util.isSuccess
import org.koitharu.kotatsu.parsers.exception.AuthRequiredException
import org.koitharu.kotatsu.parsers.exception.NotFoundException
import org.koitharu.kotatsu.parsers.model.MangaSource
@@ -23,20 +21,26 @@ import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
-class ExceptionResolver private constructor(
- private val activity: FragmentActivity?,
- private val fragment: Fragment?,
-) : ActivityResultCallback {
+class ExceptionResolver : ActivityResultCallback {
private val continuations = ArrayMap>(1)
- private lateinit var sourceAuthContract: ActivityResultLauncher
+ private val activity: FragmentActivity?
+ private val fragment: Fragment?
+ private val sourceAuthContract: ActivityResultLauncher
+ private val cloudflareContract: ActivityResultLauncher>
- constructor(activity: FragmentActivity) : this(activity = activity, fragment = null) {
+ constructor(activity: FragmentActivity) {
+ this.activity = activity
+ fragment = null
sourceAuthContract = activity.registerForActivityResult(SourceAuthActivity.Contract(), this)
+ cloudflareContract = activity.registerForActivityResult(CloudFlareActivity.Contract(), this)
}
- constructor(fragment: Fragment) : this(activity = null, fragment = fragment) {
+ constructor(fragment: Fragment) {
+ this.fragment = fragment
+ activity = null
sourceAuthContract = fragment.registerForActivityResult(SourceAuthActivity.Contract(), this)
+ cloudflareContract = fragment.registerForActivityResult(CloudFlareActivity.Contract(), this)
}
override fun onActivityResult(result: TaggedActivityResult) {
@@ -58,22 +62,9 @@ class ExceptionResolver private constructor(
else -> false
}
- private suspend fun resolveCF(url: String, headers: Headers): Boolean {
- val dialog = CloudFlareDialog.newInstance(url, headers)
- val fm = getFragmentManager()
- return suspendCancellableCoroutine { cont ->
- fm.clearFragmentResult(CloudFlareDialog.TAG)
- continuations[CloudFlareDialog.TAG] = cont
- fm.setFragmentResultListener(CloudFlareDialog.TAG, checkNotNull(fragment ?: activity)) { key, result ->
- continuations.remove(key)?.resume(result.getBoolean(CloudFlareDialog.EXTRA_RESULT))
- }
- dialog.show(fm, CloudFlareDialog.TAG)
- cont.invokeOnCancellation {
- continuations.remove(CloudFlareDialog.TAG, cont)
- fm.clearFragmentResultListener(CloudFlareDialog.TAG)
- dialog.dismissAllowingStateLoss()
- }
- }
+ private suspend fun resolveCF(url: String, headers: Headers): Boolean = suspendCoroutine { cont ->
+ continuations[CloudFlareActivity.TAG] = cont
+ cloudflareContract.launch(url to headers)
}
private suspend fun resolveAuthException(source: MangaSource): Boolean = suspendCoroutine { cont ->
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/AlertDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/AlertDialogFragment.kt
index 40c091d4d..6809043ef 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/AlertDialogFragment.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/AlertDialogFragment.kt
@@ -27,6 +27,7 @@ abstract class AlertDialogFragment : DialogFragment() {
.setView(binding.root)
.run(::onBuildDialog)
.create()
+ .also(::onDialogCreated)
}
final override fun onCreateView(
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt
index 8fba053eb..c55aaa121 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/TaggedActivityResult.kt
@@ -5,7 +5,8 @@ import android.app.Activity
class TaggedActivityResult(
val tag: String,
val result: Int,
-)
+) {
-val TaggedActivityResult.isSuccess: Boolean
- get() = this.result == Activity.RESULT_OK
+ val isSuccess: Boolean
+ get() = result == Activity.RESULT_OK
+}
diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt
index bc43c656e..e418d1d1b 100644
--- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt
+++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/String.kt
@@ -5,7 +5,7 @@ import org.koitharu.kotatsu.parsers.util.levenshteinDistance
import org.koitharu.kotatsu.util.ext.printStackTraceDebug
import java.util.UUID
-inline fun String?.ifNullOrEmpty(defaultValue: () -> String): String {
+inline fun C?.ifNullOrEmpty(defaultValue: () -> C): C {
return if (this.isNullOrEmpty()) defaultValue() else this
}
diff --git a/app/src/main/res/layout/fragment_cloudflare.xml b/app/src/main/res/layout/fragment_cloudflare.xml
deleted file mode 100644
index 91a115312..000000000
--- a/app/src/main/res/layout/fragment_cloudflare.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file