diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BaseBrowserActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BaseBrowserActivity.kt new file mode 100644 index 000000000..2f1359b43 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BaseBrowserActivity.kt @@ -0,0 +1,82 @@ +package org.koitharu.kotatsu.browser + +import android.os.Bundle +import android.view.View +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.isVisible +import androidx.core.view.updatePadding +import dagger.hilt.android.AndroidEntryPoint +import org.koitharu.kotatsu.core.network.proxy.ProxyProvider +import org.koitharu.kotatsu.core.ui.BaseActivity +import org.koitharu.kotatsu.core.util.ext.consumeAll +import org.koitharu.kotatsu.databinding.ActivityBrowserBinding +import javax.inject.Inject + +@AndroidEntryPoint +abstract class BaseBrowserActivity : BaseActivity(), BrowserCallback { + + @Inject + lateinit var proxyProvider: ProxyProvider + + private lateinit var onBackPressedCallback: WebViewBackPressedCallback + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { + return + } + viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) + onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) + onBackPressedDispatcher.addCallback(onBackPressedCallback) + } + + override fun onApplyWindowInsets( + v: View, + insets: WindowInsetsCompat + ): WindowInsetsCompat { + val type = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() + val barsInsets = insets.getInsets(type) + viewBinding.webView.updatePadding( + left = barsInsets.left, + right = barsInsets.right, + bottom = barsInsets.bottom, + ) + viewBinding.appbar.updatePadding( + left = barsInsets.left, + right = barsInsets.right, + top = barsInsets.top, + ) + return insets.consumeAll(type) + } + + override fun onPause() { + viewBinding.webView.onPause() + super.onPause() + } + + override fun onResume() { + super.onResume() + viewBinding.webView.onResume() + } + + override fun onDestroy() { + super.onDestroy() + if (hasViewBinding()) { + viewBinding.webView.stopLoading() + viewBinding.webView.destroy() + } + } + + override fun onLoadingStateChanged(isLoading: Boolean) { + viewBinding.progressBar.isVisible = isLoading + } + + override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) { + this.title = title + supportActionBar?.subtitle = subtitle + } + + override fun onHistoryChanged() { + onBackPressedCallback.onHistoryChanged() + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt index 758a8299b..110e1c1dc 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserActivity.kt @@ -3,12 +3,10 @@ package org.koitharu.kotatsu.browser import android.os.Bundle import android.view.Menu import android.view.MenuItem -import android.view.View -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.isVisible -import androidx.core.view.updatePadding +import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.nav.AppRouter @@ -16,26 +14,20 @@ import org.koitharu.kotatsu.core.nav.router import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.ParserMangaRepository -import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.util.ext.configureForParser -import org.koitharu.kotatsu.core.util.ext.consumeAll -import org.koitharu.kotatsu.databinding.ActivityBrowserBinding +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import javax.inject.Inject import com.google.android.material.R as materialR @AndroidEntryPoint -class BrowserActivity : BaseActivity(), BrowserCallback { - - private lateinit var onBackPressedCallback: WebViewBackPressedCallback +class BrowserActivity : BaseBrowserActivity() { @Inject lateinit var mangaRepositoryFactory: MangaRepository.Factory override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { - return - } supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) @@ -44,42 +36,27 @@ class BrowserActivity : BaseActivity(), BrowserCallback val repository = mangaRepositoryFactory.create(mangaSource) as? ParserMangaRepository val userAgent = repository?.getRequestHeaders()?.get(CommonHeaders.USER_AGENT) viewBinding.webView.configureForParser(userAgent) - viewBinding.webView.webViewClient = BrowserClient(this) - viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) - onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) - onBackPressedDispatcher.addCallback(onBackPressedCallback) - if (savedInstanceState != null) { - return + viewBinding.webView.webViewClient = BrowserClient(proxyProvider, this) + lifecycleScope.launch { + try { + proxyProvider.applyWebViewConfig() + } catch (e: Exception) { + e.printStackTraceDebug() + Snackbar.make(viewBinding.webView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() + } + if (savedInstanceState == null) { + val url = intent?.dataString + if (url.isNullOrEmpty()) { + finishAfterTransition() + } else { + onTitleChanged( + intent?.getStringExtra(AppRouter.KEY_TITLE) ?: getString(R.string.loading_), + url, + ) + viewBinding.webView.loadUrl(url) + } + } } - val url = intent?.dataString - if (url.isNullOrEmpty()) { - finishAfterTransition() - } else { - onTitleChanged( - intent?.getStringExtra(AppRouter.KEY_TITLE) ?: getString(R.string.loading_), - url, - ) - viewBinding.webView.loadUrl(url) - } - } - - override fun onApplyWindowInsets( - v: View, - insets: WindowInsetsCompat - ): WindowInsetsCompat { - val type = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() - val barsInsets = insets.getInsets(type) - viewBinding.webView.updatePadding( - left = barsInsets.left, - right = barsInsets.right, - bottom = barsInsets.bottom, - ) - viewBinding.appbar.updatePadding( - left = barsInsets.left, - right = barsInsets.right, - top = barsInsets.top, - ) - return insets.consumeAll(type) } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -104,35 +81,4 @@ class BrowserActivity : BaseActivity(), BrowserCallback else -> super.onOptionsItemSelected(item) } - - override fun onPause() { - viewBinding.webView.onPause() - super.onPause() - } - - override fun onResume() { - super.onResume() - viewBinding.webView.onResume() - } - - override fun onDestroy() { - super.onDestroy() - if (hasViewBinding()) { - viewBinding.webView.stopLoading() - viewBinding.webView.destroy() - } - } - - override fun onLoadingStateChanged(isLoading: Boolean) { - viewBinding.progressBar.isVisible = isLoading - } - - override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) { - this.title = title - supportActionBar?.subtitle = subtitle - } - - override fun onHistoryChanged() { - onBackPressedCallback.onHistoryChanged() - } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserClient.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserClient.kt index e6906014e..9d72bcfe7 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserClient.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/BrowserClient.kt @@ -2,9 +2,13 @@ package org.koitharu.kotatsu.browser import android.graphics.Bitmap import android.webkit.WebView -import android.webkit.WebViewClient +import androidx.webkit.WebViewClientCompat +import org.koitharu.kotatsu.core.network.proxy.ProxyProvider -open class BrowserClient(private val callback: BrowserCallback) : WebViewClient() { +open class BrowserClient( + private val proxyProvider: ProxyProvider, + private val callback: BrowserCallback +) : WebViewClientCompat() { override fun onPageFinished(webView: WebView, url: String) { super.onPageFinished(webView, url) @@ -16,7 +20,7 @@ open class BrowserClient(private val callback: BrowserCallback) : WebViewClient( callback.onLoadingStateChanged(isLoading = true) } - override fun onPageCommitVisible(view: WebView, url: String?) { + override fun onPageCommitVisible(view: WebView, url: String) { super.onPageCommitVisible(view, url) callback.onTitleChanged(view.title.orEmpty(), url) } 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 index d3e7b3d55..c97a94548 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareActivity.kt @@ -5,13 +5,10 @@ import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem -import android.view.View import androidx.activity.result.contract.ActivityResultContract -import androidx.core.view.WindowInsetsCompat import androidx.core.view.isInvisible -import androidx.core.view.isVisible -import androidx.core.view.updatePadding import androidx.lifecycle.lifecycleScope +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -20,21 +17,19 @@ import kotlinx.coroutines.yield import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.browser.WebViewBackPressedCallback +import org.koitharu.kotatsu.browser.BaseBrowserActivity import org.koitharu.kotatsu.core.exceptions.CloudFlareProtectedException import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar -import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.util.ext.configureForParser -import org.koitharu.kotatsu.core.util.ext.consumeAll -import org.koitharu.kotatsu.databinding.ActivityBrowserBinding +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.parsers.network.CloudFlareHelper import javax.inject.Inject import com.google.android.material.R as materialR @AndroidEntryPoint -class CloudFlareActivity : BaseActivity(), CloudFlareCallback { +class CloudFlareActivity : BaseBrowserActivity(), CloudFlareCallback { private var pendingResult = RESULT_CANCELED @@ -42,13 +37,9 @@ class CloudFlareActivity : BaseActivity(), CloudFlareCal lateinit var cookieJar: MutableCookieJar private lateinit var cfClient: CloudFlareClient - private var onBackPressedCallback: WebViewBackPressedCallback? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { - return - } supportActionBar?.run { setDisplayHomeAsUpEnabled(true) setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) @@ -58,45 +49,20 @@ class CloudFlareActivity : BaseActivity(), CloudFlareCal finishAfterTransition() return } - cfClient = CloudFlareClient(cookieJar, this, url) + cfClient = CloudFlareClient(proxyProvider, cookieJar, this, url) viewBinding.webView.configureForParser(intent?.getStringExtra(AppRouter.KEY_USER_AGENT)) viewBinding.webView.webViewClient = cfClient - onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView).also { - onBackPressedDispatcher.addCallback(it) + lifecycleScope.launch { + try { + proxyProvider.applyWebViewConfig() + } catch (e: Exception) { + Snackbar.make(viewBinding.webView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() + } + if (savedInstanceState == null) { + onTitleChanged(getString(R.string.loading_), url) + viewBinding.webView.loadUrl(url) + } } - if (savedInstanceState == null) { - onTitleChanged(getString(R.string.loading_), url) - viewBinding.webView.loadUrl(url) - } - } - - override fun onApplyWindowInsets( - v: View, - insets: WindowInsetsCompat - ): WindowInsetsCompat { - val type = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() - val barsInsets = insets.getInsets(type) - viewBinding.webView.updatePadding( - left = barsInsets.left, - right = barsInsets.right, - bottom = barsInsets.bottom, - ) - viewBinding.appbar.updatePadding( - left = barsInsets.left, - right = barsInsets.right, - top = barsInsets.top, - ) - return insets.consumeAll(type) - } - - override fun onDestroy() { - runCatching { - viewBinding.webView - }.onSuccess { - it.stopLoading() - it.destroy() - } - super.onDestroy() } override fun onCreateOptionsMenu(menu: Menu?): Boolean { @@ -119,21 +85,13 @@ class CloudFlareActivity : BaseActivity(), CloudFlareCal 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 onLoadingStateChanged(isLoading: Boolean) = Unit + override fun onPageLoaded() { viewBinding.progressBar.isInvisible = true } @@ -151,14 +109,6 @@ class CloudFlareActivity : BaseActivity(), CloudFlareCal 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 = diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareCallback.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareCallback.kt index b38527c6a..83e86d900 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareCallback.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareCallback.kt @@ -4,8 +4,6 @@ import org.koitharu.kotatsu.browser.BrowserCallback interface CloudFlareCallback : BrowserCallback { - override fun onLoadingStateChanged(isLoading: Boolean) = Unit - override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) = Unit fun onPageLoaded() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt index e5bd0c5cb..18c353c03 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/browser/cloudflare/CloudFlareClient.kt @@ -4,15 +4,17 @@ import android.graphics.Bitmap import android.webkit.WebView import org.koitharu.kotatsu.browser.BrowserClient import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar +import org.koitharu.kotatsu.core.network.proxy.ProxyProvider import org.koitharu.kotatsu.parsers.network.CloudFlareHelper private const val LOOP_COUNTER = 3 class CloudFlareClient( + proxyProvider: ProxyProvider, private val cookieJar: MutableCookieJar, private val callback: CloudFlareCallback, private val targetUrl: String, -) : BrowserClient(callback) { +) : BrowserClient(proxyProvider, callback) { private val oldClearance = getClearance() private var counter = 0 @@ -22,7 +24,7 @@ class CloudFlareClient( checkClearance() } - override fun onPageCommitVisible(view: WebView, url: String?) { + override fun onPageCommitVisible(view: WebView, url: String) { super.onPageCommitVisible(view, url) callback.onPageLoaded() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/AppProxySelector.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/AppProxySelector.kt deleted file mode 100644 index 4ac33ad07..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/AppProxySelector.kt +++ /dev/null @@ -1,51 +0,0 @@ -package org.koitharu.kotatsu.core.network - -import okio.IOException -import org.koitharu.kotatsu.core.exceptions.ProxyConfigException -import org.koitharu.kotatsu.core.prefs.AppSettings -import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug -import java.net.InetSocketAddress -import java.net.Proxy -import java.net.ProxySelector -import java.net.SocketAddress -import java.net.URI - -class AppProxySelector( - private val settings: AppSettings, -) : ProxySelector() { - - init { - setDefault(this) - } - - private var cachedProxy: Proxy? = null - - override fun select(uri: URI?): List { - return listOf(getProxy()) - } - - override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: IOException?) { - ioe?.printStackTraceDebug() - } - - private fun getProxy(): Proxy { - val type = settings.proxyType - val address = settings.proxyAddress - val port = settings.proxyPort - if (type == Proxy.Type.DIRECT) { - return Proxy.NO_PROXY - } - if (address.isNullOrEmpty() || port < 0 || port > 0xFFFF) { - throw ProxyConfigException() - } - cachedProxy?.let { - val addr = it.address() as? InetSocketAddress - if (addr != null && it.type() == type && addr.port == port && addr.hostString == address) { - return it - } - } - val proxy = Proxy(type, InetSocketAddress(address, port)) - cachedProxy = proxy - return proxy - } -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt index 0200b99c4..7711545ea 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/NetworkModule.kt @@ -16,6 +16,7 @@ import org.koitharu.kotatsu.core.network.cookies.MutableCookieJar import org.koitharu.kotatsu.core.network.cookies.PreferencesCookieJar import org.koitharu.kotatsu.core.network.imageproxy.ImageProxyInterceptor import org.koitharu.kotatsu.core.network.imageproxy.RealImageProxyInterceptor +import org.koitharu.kotatsu.core.network.proxy.ProxyProvider import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.util.ext.assertNotInMainThread import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug @@ -62,14 +63,15 @@ interface NetworkModule { cache: Cache, cookieJar: CookieJar, settings: AppSettings, + proxyProvider: ProxyProvider, ): OkHttpClient = OkHttpClient.Builder().apply { assertNotInMainThread() connectTimeout(20, TimeUnit.SECONDS) readTimeout(60, TimeUnit.SECONDS) writeTimeout(20, TimeUnit.SECONDS) cookieJar(cookieJar) - proxySelector(AppProxySelector(settings)) - proxyAuthenticator(ProxyAuthenticator(settings)) + proxySelector(proxyProvider.selector) + proxyAuthenticator(proxyProvider.authenticator) dns(DoHManager(cache, settings)) if (settings.isSSLBypassEnabled) { disableCertificateVerification() diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ProxyAuthenticator.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ProxyAuthenticator.kt deleted file mode 100644 index fb4ffad7e..000000000 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/ProxyAuthenticator.kt +++ /dev/null @@ -1,45 +0,0 @@ -package org.koitharu.kotatsu.core.network - -import okhttp3.Authenticator -import okhttp3.Credentials -import okhttp3.Request -import okhttp3.Response -import okhttp3.Route -import org.koitharu.kotatsu.core.prefs.AppSettings -import java.net.PasswordAuthentication -import java.net.Proxy - -class ProxyAuthenticator( - private val settings: AppSettings, -) : Authenticator, java.net.Authenticator() { - - init { - setDefault(this) - } - - override fun authenticate(route: Route?, response: Response): Request? { - if (!isProxyEnabled()) { - return null - } - if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) { - return null - } - val login = settings.proxyLogin ?: return null - val password = settings.proxyPassword ?: return null - val credential = Credentials.basic(login, password) - return response.request.newBuilder() - .header(CommonHeaders.PROXY_AUTHORIZATION, credential) - .build() - } - - override fun getPasswordAuthentication(): PasswordAuthentication? { - if (!isProxyEnabled()) { - return null - } - val login = settings.proxyLogin ?: return null - val password = settings.proxyPassword ?: return null - return PasswordAuthentication(login, password.toCharArray()) - } - - private fun isProxyEnabled() = settings.proxyType != Proxy.Type.DIRECT -} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/network/proxy/ProxyProvider.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/proxy/ProxyProvider.kt new file mode 100644 index 000000000..7b6df3c5e --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/network/proxy/ProxyProvider.kt @@ -0,0 +1,150 @@ +package org.koitharu.kotatsu.core.network.proxy + +import androidx.webkit.ProxyConfig +import androidx.webkit.ProxyController +import androidx.webkit.WebViewFeature +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import okhttp3.Authenticator +import okhttp3.Credentials +import okhttp3.Request +import okhttp3.Response +import okhttp3.Route +import org.koitharu.kotatsu.core.exceptions.ProxyConfigException +import org.koitharu.kotatsu.core.network.CommonHeaders +import org.koitharu.kotatsu.core.prefs.AppSettings +import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug +import java.net.InetSocketAddress +import java.net.PasswordAuthentication +import java.net.Proxy +import java.net.ProxySelector +import java.net.SocketAddress +import java.net.URI +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import java.net.Authenticator as JavaAuthenticator + +@Singleton +class ProxyProvider @Inject constructor( + private val settings: AppSettings, +) { + + private var cachedProxy: Proxy? = null + + val selector = object : ProxySelector() { + override fun select(uri: URI?): List { + return listOf(getProxy()) + } + + override fun connectFailed(uri: URI?, sa: SocketAddress?, ioe: okio.IOException?) { + ioe?.printStackTraceDebug() + } + } + + val authenticator = ProxyAuthenticator() + + init { + ProxySelector.setDefault(selector) + JavaAuthenticator.setDefault(authenticator) + } + + suspend fun applyWebViewConfig() { + val isProxyEnabled = isProxyEnabled() + if (!WebViewFeature.isFeatureSupported(WebViewFeature.PROXY_OVERRIDE)) { + if (isProxyEnabled) { + throw IllegalArgumentException("Proxy for WebView is not supported") // TODO localize + } + } else { + val controller = ProxyController.getInstance() + if (settings.proxyType == Proxy.Type.DIRECT) { + suspendCoroutine { cont -> + controller.clearProxyOverride( + (cont.context[CoroutineDispatcher] ?: Dispatchers.Main).asExecutor(), + ) { + cont.resume(Unit) + } + } + } else { + val url = buildString { + when (settings.proxyType) { + Proxy.Type.DIRECT -> Unit + Proxy.Type.HTTP -> append("http") + Proxy.Type.SOCKS -> append("socks") + } + append("://") + append(settings.proxyAddress) + append(':') + append(settings.proxyPort) + } + if (settings.proxyType == Proxy.Type.SOCKS) { + System.setProperty("java.net.socks.username", settings.proxyLogin); + System.setProperty("java.net.socks.password", settings.proxyPassword); + } + val proxyConfig = ProxyConfig.Builder() + .addProxyRule(url) + .build() + suspendCoroutine { cont -> + controller.setProxyOverride( + proxyConfig, + (cont.context[CoroutineDispatcher] ?: Dispatchers.Main).asExecutor(), + ) { + cont.resume(Unit) + } + } + } + } + } + + private fun isProxyEnabled() = settings.proxyType != Proxy.Type.DIRECT + + private fun getProxy(): Proxy { + val type = settings.proxyType + val address = settings.proxyAddress + val port = settings.proxyPort + if (type == Proxy.Type.DIRECT) { + return Proxy.NO_PROXY + } + if (address.isNullOrEmpty() || port < 0 || port > 0xFFFF) { + throw ProxyConfigException() + } + cachedProxy?.let { + val addr = it.address() as? InetSocketAddress + if (addr != null && it.type() == type && addr.port == port && addr.hostString == address) { + return it + } + } + val proxy = Proxy(type, InetSocketAddress(address, port)) + cachedProxy = proxy + return proxy + } + + inner class ProxyAuthenticator : Authenticator, JavaAuthenticator() { + + override fun authenticate(route: Route?, response: Response): Request? { + if (!isProxyEnabled()) { + return null + } + if (response.request.header(CommonHeaders.PROXY_AUTHORIZATION) != null) { + return null + } + val login = settings.proxyLogin ?: return null + val password = settings.proxyPassword ?: return null + val credential = Credentials.basic(login, password) + return response.request.newBuilder() + .header(CommonHeaders.PROXY_AUTHORIZATION, credential) + .build() + } + + public override fun getPasswordAuthentication(): PasswordAuthentication? { + if (!isProxyEnabled()) { + return null + } + val login = settings.proxyLogin ?: return null + val password = settings.proxyPassword ?: return null + return PasswordAuthentication(login, password.toCharArray()) + } + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ProxySettingsFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ProxySettingsFragment.kt index c0203869f..cd630618b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/ProxySettingsFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/ProxySettingsFragment.kt @@ -43,6 +43,7 @@ class ProxySettingsFragment : BasePreferenceFragment(R.string.proxy), override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_proxy) + @Suppress("UsePropertyAccessSyntax") findPreference(AppSettings.KEY_PROXY_ADDRESS)?.setOnBindEditTextListener( EditTextBindListener( inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_URI, @@ -50,6 +51,7 @@ class ProxySettingsFragment : BasePreferenceFragment(R.string.proxy), validator = DomainValidator(), ), ) + @Suppress("UsePropertyAccessSyntax") findPreference(AppSettings.KEY_PROXY_PORT)?.setOnBindEditTextListener( EditTextBindListener( inputType = EditorInfo.TYPE_CLASS_NUMBER, @@ -58,6 +60,7 @@ class ProxySettingsFragment : BasePreferenceFragment(R.string.proxy), ), ) findPreference(AppSettings.KEY_PROXY_PASSWORD)?.let { pref -> + @Suppress("UsePropertyAccessSyntax") pref.setOnBindEditTextListener( EditTextBindListener( inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_VARIATION_PASSWORD, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt index 3ac2782f6..b78817638 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/sources/auth/SourceAuthActivity.kt @@ -1,30 +1,26 @@ package org.koitharu.kotatsu.settings.sources.auth -import android.annotation.SuppressLint import android.content.Context import android.content.Intent import android.os.Bundle import android.view.MenuItem -import android.view.View import android.widget.Toast import androidx.activity.result.contract.ActivityResultContract -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.isVisible -import androidx.core.view.updatePadding +import androidx.lifecycle.lifecycleScope +import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.browser.BaseBrowserActivity import org.koitharu.kotatsu.browser.BrowserCallback import org.koitharu.kotatsu.browser.BrowserClient -import org.koitharu.kotatsu.browser.ProgressChromeClient -import org.koitharu.kotatsu.browser.WebViewBackPressedCallback import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.nav.AppRouter import org.koitharu.kotatsu.core.network.CommonHeaders import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.parser.ParserMangaRepository -import org.koitharu.kotatsu.core.ui.BaseActivity import org.koitharu.kotatsu.core.util.ext.configureForParser -import org.koitharu.kotatsu.core.util.ext.consumeAll +import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.databinding.ActivityBrowserBinding import org.koitharu.kotatsu.parsers.MangaParserAuthProvider import org.koitharu.kotatsu.parsers.model.MangaParserSource @@ -34,15 +30,13 @@ import javax.inject.Inject import com.google.android.material.R as materialR @AndroidEntryPoint -class SourceAuthActivity : BaseActivity(), BrowserCallback { +class SourceAuthActivity : BaseBrowserActivity(), BrowserCallback { @Inject lateinit var mangaRepositoryFactory: MangaRepository.Factory - private lateinit var onBackPressedCallback: WebViewBackPressedCallback private lateinit var authProvider: MangaParserAuthProvider - @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!setContentViewWebViewSafe { ActivityBrowserBinding.inflate(layoutInflater) }) { @@ -68,43 +62,22 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba setHomeAsUpIndicator(materialR.drawable.abc_ic_clear_material) } viewBinding.webView.configureForParser(repository.getRequestHeaders()[CommonHeaders.USER_AGENT]) - viewBinding.webView.webViewClient = BrowserClient(this) - viewBinding.webView.webChromeClient = ProgressChromeClient(viewBinding.progressBar) - onBackPressedCallback = WebViewBackPressedCallback(viewBinding.webView) - onBackPressedDispatcher.addCallback(onBackPressedCallback) - if (savedInstanceState != null) { - return + viewBinding.webView.webViewClient = BrowserClient(proxyProvider, this) + lifecycleScope.launch { + try { + proxyProvider.applyWebViewConfig() + } catch (e: Exception) { + Snackbar.make(viewBinding.webView, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() + } + if (savedInstanceState == null) { + val url = authProvider.authUrl + onTitleChanged( + source.title, + getString(R.string.loading_), + ) + viewBinding.webView.loadUrl(url) + } } - val url = authProvider.authUrl - onTitleChanged( - source.title, - getString(R.string.loading_), - ) - viewBinding.webView.loadUrl(url) - } - - override fun onDestroy() { - super.onDestroy() - viewBinding.webView.destroy() - } - - override fun onApplyWindowInsets( - v: View, - insets: WindowInsetsCompat - ): WindowInsetsCompat { - val type = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime() - val barsInsets = insets.getInsets(type) - viewBinding.webView.updatePadding( - left = barsInsets.left, - right = barsInsets.right, - bottom = barsInsets.bottom, - ) - viewBinding.appbar.updatePadding( - left = barsInsets.left, - right = barsInsets.right, - top = barsInsets.top, - ) - return insets.consumeAll(type) } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { @@ -118,18 +91,8 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba else -> super.onOptionsItemSelected(item) } - override fun onPause() { - viewBinding.webView.onPause() - super.onPause() - } - - override fun onResume() { - super.onResume() - viewBinding.webView.onResume() - } - override fun onLoadingStateChanged(isLoading: Boolean) { - viewBinding.progressBar.isVisible = isLoading + super.onLoadingStateChanged(isLoading) if (!isLoading && authProvider.isAuthorized) { Toast.makeText(this, R.string.auth_complete, Toast.LENGTH_SHORT).show() setResult(RESULT_OK) @@ -137,15 +100,6 @@ class SourceAuthActivity : BaseActivity(), BrowserCallba } } - override fun onTitleChanged(title: CharSequence, subtitle: CharSequence?) { - this.title = title - supportActionBar?.subtitle = subtitle - } - - override fun onHistoryChanged() { - onBackPressedCallback.onHistoryChanged() - } - class Contract : ActivityResultContract() { override fun createIntent(context: Context, input: MangaSource): Intent { return AppRouter.sourceAuthIntent(context, input) diff --git a/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt b/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt index b4d2667ca..e0119a8c9 100644 --- a/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt +++ b/app/src/test/kotlin/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt @@ -7,6 +7,7 @@ import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.history.data.HistoryEntity +import org.koitharu.kotatsu.parsers.model.ContentRating import org.koitharu.kotatsu.parsers.model.MangaParserSource import org.koitharu.kotatsu.parsers.model.MangaState import org.koitharu.kotatsu.parsers.model.SortOrder @@ -33,15 +34,16 @@ class JsonSerializerTest { val entity = MangaEntity( id = 231, title = "Lorem Ipsum", - altTitle = "Lorem Ispum 2", + altTitles = "Lorem Ispum 2", url = "erw", publicUrl = "hthth", rating = 0.78f, isNsfw = true, + contentRating = ContentRating.ADULT.name, coverUrl = "5345", largeCoverUrl = null, state = MangaState.FINISHED.name, - author = "RERE", + authors = "RERE", source = MangaParserSource.DUMMY.name, ) val json = JsonSerializer(entity).toJson()