mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-23 21:27:40 +03:00
Clean up interceptors a bit
This commit is contained in:
parent
fddca15182
commit
dc62d0ea8b
5 changed files with 116 additions and 127 deletions
|
@ -17,12 +17,13 @@ class NetworkHelper(context: Context) {
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
private val cacheDir = File(context.cacheDir, "network_cache")
|
private val cacheDir = File(context.cacheDir, "network_cache")
|
||||||
|
|
||||||
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
|
||||||
|
|
||||||
val cookieManager = AndroidCookieJar()
|
val cookieManager = AndroidCookieJar()
|
||||||
|
|
||||||
private val http103Interceptor = Http103Interceptor(context)
|
private val userAgentInterceptor by lazy { UserAgentInterceptor() }
|
||||||
|
private val http103Interceptor by lazy { Http103Interceptor(context) }
|
||||||
|
private val cloudflareInterceptor by lazy { CloudflareInterceptor(context) }
|
||||||
|
|
||||||
private val baseClientBuilder: OkHttpClient.Builder
|
private val baseClientBuilder: OkHttpClient.Builder
|
||||||
get() {
|
get() {
|
||||||
|
@ -32,7 +33,7 @@ class NetworkHelper(context: Context) {
|
||||||
.readTimeout(30, TimeUnit.SECONDS)
|
.readTimeout(30, TimeUnit.SECONDS)
|
||||||
.callTimeout(2, TimeUnit.MINUTES)
|
.callTimeout(2, TimeUnit.MINUTES)
|
||||||
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
|
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
|
||||||
.addInterceptor(UserAgentInterceptor())
|
.addInterceptor(userAgentInterceptor)
|
||||||
.addNetworkInterceptor(http103Interceptor)
|
.addNetworkInterceptor(http103Interceptor)
|
||||||
|
|
||||||
if (preferences.verboseLogging()) {
|
if (preferences.verboseLogging()) {
|
||||||
|
@ -64,7 +65,7 @@ class NetworkHelper(context: Context) {
|
||||||
@Suppress("UNUSED")
|
@Suppress("UNUSED")
|
||||||
val cloudflareClient by lazy {
|
val cloudflareClient by lazy {
|
||||||
client.newBuilder()
|
client.newBuilder()
|
||||||
.addInterceptor(CloudflareInterceptor(context))
|
.addInterceptor(cloudflareInterceptor)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,12 @@ package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.webkit.WebSettings
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.isOutdated
|
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||||
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
@ -26,56 +21,26 @@ import java.io.IOException
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
class CloudflareInterceptor(private val context: Context) : WebViewInterceptor(context) {
|
||||||
|
|
||||||
private val executor = ContextCompat.getMainExecutor(context)
|
private val executor = ContextCompat.getMainExecutor(context)
|
||||||
|
|
||||||
private val networkHelper: NetworkHelper by injectLazy()
|
private val networkHelper: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
/**
|
override fun shouldIntercept(response: Response): Boolean {
|
||||||
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
// Check if Cloudflare anti-bot is on
|
||||||
* blocking the main thread too much. If used too often we could consider moving it to the
|
return response.code in ERROR_CODES && response.header("Server") in SERVER_CHECK
|
||||||
* Application class.
|
|
||||||
*/
|
|
||||||
private val initWebView by lazy {
|
|
||||||
// Crashes on some devices. We skip this in some cases since the only impact is slower
|
|
||||||
// WebView init in those rare cases.
|
|
||||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
|
|
||||||
if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
|
|
||||||
return@lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
WebSettings.getDefaultUserAgent(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
|
||||||
val originalRequest = chain.request()
|
|
||||||
|
|
||||||
if (!WebViewUtil.supportsWebView(context)) {
|
|
||||||
launchUI {
|
|
||||||
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
|
||||||
}
|
|
||||||
return chain.proceed(originalRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
initWebView
|
|
||||||
|
|
||||||
val response = chain.proceed(originalRequest)
|
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
|
||||||
if (response.code !in ERROR_CODES || response.header("Server") !in SERVER_CHECK) {
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response.close()
|
response.close()
|
||||||
networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
|
networkHelper.cookieManager.remove(request.url, COOKIE_NAMES, 0)
|
||||||
val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
|
val oldCookie = networkHelper.cookieManager.get(request.url)
|
||||||
.firstOrNull { it.name == "cf_clearance" }
|
.firstOrNull { it.name == "cf_clearance" }
|
||||||
resolveWithWebView(originalRequest, oldCookie)
|
resolveWithWebView(request, oldCookie)
|
||||||
|
|
||||||
return chain.proceed(originalRequest)
|
return chain.proceed(request)
|
||||||
}
|
}
|
||||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||||
// we don't crash the entire app
|
// we don't crash the entire app
|
||||||
|
@ -87,7 +52,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
|
private fun resolveWithWebView(originalRequest: Request, oldCookie: Cookie?) {
|
||||||
// We need to lock this thread until the WebView finds the challenge solution url, because
|
// We need to lock this thread until the WebView finds the challenge solution url, because
|
||||||
// OkHttp doesn't support asynchronous interceptors.
|
// OkHttp doesn't support asynchronous interceptors.
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
|
@ -98,8 +63,8 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
var cloudflareBypassed = false
|
var cloudflareBypassed = false
|
||||||
var isWebViewOutdated = false
|
var isWebViewOutdated = false
|
||||||
|
|
||||||
val origRequestUrl = request.url.toString()
|
val origRequestUrl = originalRequest.url.toString()
|
||||||
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||||
|
|
||||||
executor.execute {
|
executor.execute {
|
||||||
val webview = WebView(context)
|
val webview = WebView(context)
|
||||||
|
@ -107,7 +72,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
webview.setDefaultSettings()
|
webview.setDefaultSettings()
|
||||||
|
|
||||||
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
|
||||||
webview.settings.userAgentString = request.header("User-Agent")
|
webview.settings.userAgentString = originalRequest.header("User-Agent")
|
||||||
?: networkHelper.defaultUserAgent
|
?: networkHelper.defaultUserAgent
|
||||||
|
|
||||||
webview.webViewClient = object : WebViewClientCompat() {
|
webview.webViewClient = object : WebViewClientCompat() {
|
||||||
|
@ -175,12 +140,10 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
throw CloudflareBypassException()
|
throw CloudflareBypassException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val ERROR_CODES = listOf(403, 503)
|
|
||||||
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
|
||||||
private val COOKIE_NAMES = listOf("cf_clearance")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val ERROR_CODES = listOf(403, 503)
|
||||||
|
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
|
private val COOKIE_NAMES = listOf("cf_clearance")
|
||||||
|
|
||||||
private class CloudflareBypassException : Exception()
|
private class CloudflareBypassException : Exception()
|
||||||
|
|
|
@ -2,67 +2,31 @@ package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebSettings
|
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
|
||||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
|
||||||
import eu.kanade.tachiyomi.util.system.logcat
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.Protocol
|
import okhttp3.Protocol
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
// TODO: Remove when OkHttp can handle http 103 responses
|
// TODO: Remove when OkHttp can handle HTTP 103 responses
|
||||||
class Http103Interceptor(private val context: Context) : Interceptor {
|
class Http103Interceptor(context: Context) : WebViewInterceptor(context) {
|
||||||
|
|
||||||
private val executor = ContextCompat.getMainExecutor(context)
|
private val executor = ContextCompat.getMainExecutor(context)
|
||||||
|
|
||||||
private val networkHelper: NetworkHelper by injectLazy()
|
override fun shouldIntercept(response: Response): Boolean {
|
||||||
|
return response.code == 103
|
||||||
/**
|
|
||||||
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
|
||||||
* blocking the main thread too much. If used too often we could consider moving it to the
|
|
||||||
* Application class.
|
|
||||||
*/
|
|
||||||
private val initWebView by lazy {
|
|
||||||
// Crashes on some devices. We skip this in some cases since the only impact is slower
|
|
||||||
// WebView init in those rare cases.
|
|
||||||
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
|
|
||||||
if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
|
|
||||||
return@lazy
|
|
||||||
}
|
|
||||||
|
|
||||||
WebSettings.getDefaultUserAgent(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response {
|
||||||
val request = chain.request()
|
|
||||||
val response = chain.proceed(request)
|
|
||||||
if (response.code != 103) return response
|
|
||||||
if (!WebViewUtil.supportsWebView(context)) {
|
|
||||||
launchUI {
|
|
||||||
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
initWebView
|
|
||||||
|
|
||||||
logcat { "Proceeding with WebView for request $request" }
|
logcat { "Proceeding with WebView for request $request" }
|
||||||
try {
|
try {
|
||||||
return proceedWithWebView(request, response)
|
return proceedWithWebView(request, response)
|
||||||
|
@ -71,23 +35,9 @@ class Http103Interceptor(private val context: Context) : Interceptor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class JsInterface(private val latch: CountDownLatch, var payload: String? = null) {
|
|
||||||
@JavascriptInterface
|
|
||||||
fun passPayload(passedPayload: String) {
|
|
||||||
payload = passedPayload
|
|
||||||
latch.countDown()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val jsScript = "window.android.passPayload(document.querySelector('html').outerHTML)"
|
|
||||||
|
|
||||||
val htmlMediaType = "text/html".toMediaType()
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
|
@SuppressLint("SetJavaScriptEnabled", "AddJavascriptInterface")
|
||||||
private fun proceedWithWebView(ogRequest: Request, ogResponse: Response): Response {
|
private fun proceedWithWebView(originalRequest: Request, originalResponse: Response): Response {
|
||||||
// We need to lock this thread until the WebView finds the challenge solution url, because
|
// We need to lock this thread until the WebView loads the page, because
|
||||||
// OkHttp doesn't support asynchronous interceptors.
|
// OkHttp doesn't support asynchronous interceptors.
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
|
@ -97,16 +47,11 @@ class Http103Interceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
|
|
||||||
val requestUrl = ogRequest.url.toString()
|
val requestUrl = originalRequest.url.toString()
|
||||||
val headers = ogRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
val headers = originalRequest.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }.toMutableMap()
|
||||||
|
|
||||||
executor.execute {
|
executor.execute {
|
||||||
val webview = WebView(context).also { outerWebView = it }
|
val webview = createWebView(originalRequest).also { outerWebView = it }
|
||||||
with(webview.settings) {
|
|
||||||
javaScriptEnabled = true
|
|
||||||
userAgentString = ogRequest.header("User-Agent") ?: networkHelper.defaultUserAgent
|
|
||||||
}
|
|
||||||
|
|
||||||
webview.addJavascriptInterface(jsInterface, "android")
|
webview.addJavascriptInterface(jsInterface, "android")
|
||||||
|
|
||||||
webview.webViewClient = object : WebViewClientCompat() {
|
webview.webViewClient = object : WebViewClientCompat() {
|
||||||
|
@ -143,13 +88,25 @@ class Http103Interceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
exception?.let { throw it }
|
exception?.let { throw it }
|
||||||
|
|
||||||
val payload = jsInterface.payload ?: throw Exception("Couldn't fetch site through webview")
|
val responseHtml = jsInterface.responseHtml ?: throw Exception("Couldn't fetch site through webview")
|
||||||
|
|
||||||
return ogResponse.newBuilder()
|
return originalResponse.newBuilder()
|
||||||
.code(200)
|
.code(200)
|
||||||
.protocol(Protocol.HTTP_1_1)
|
.protocol(Protocol.HTTP_1_1)
|
||||||
.message("OK")
|
.message("OK")
|
||||||
.body(payload.toResponseBody(htmlMediaType))
|
.body(responseHtml.toResponseBody(htmlMediaType))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class JsInterface(private val latch: CountDownLatch, var responseHtml: String? = null) {
|
||||||
|
@Suppress("UNUSED")
|
||||||
|
@JavascriptInterface
|
||||||
|
fun passPayload(passedPayload: String) {
|
||||||
|
responseHtml = passedPayload
|
||||||
|
latch.countDown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val jsScript = "window.android.passPayload(document.querySelector('html').outerHTML)"
|
||||||
|
private val htmlMediaType = "text/html".toMediaType()
|
||||||
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
package eu.kanade.tachiyomi.network.interceptor
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import android.webkit.WebSettings
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.widget.Toast
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||||
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||||
|
import eu.kanade.tachiyomi.util.system.setDefaultSettings
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
|
abstract class WebViewInterceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
|
private val networkHelper: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When this is called, it initializes the WebView if it wasn't already. We use this to avoid
|
||||||
|
* blocking the main thread too much. If used too often we could consider moving it to the
|
||||||
|
* Application class.
|
||||||
|
*/
|
||||||
|
private val initWebView by lazy {
|
||||||
|
// Crashes on some devices. We skip this in some cases since the only impact is slower
|
||||||
|
// WebView init in those rare cases.
|
||||||
|
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
|
||||||
|
if (DeviceUtil.isMiui || Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
|
||||||
|
return@lazy
|
||||||
|
}
|
||||||
|
|
||||||
|
WebSettings.getDefaultUserAgent(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun shouldIntercept(response: Response): Boolean
|
||||||
|
|
||||||
|
abstract fun intercept(chain: Interceptor.Chain, request: Request, response: Response): Response
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val request = chain.request()
|
||||||
|
val response = chain.proceed(request)
|
||||||
|
if (!shouldIntercept(response)) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WebViewUtil.supportsWebView(context)) {
|
||||||
|
launchUI {
|
||||||
|
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
initWebView
|
||||||
|
|
||||||
|
return intercept(chain, request, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createWebView(request: Request): WebView {
|
||||||
|
val webview = WebView(context)
|
||||||
|
webview.setDefaultSettings()
|
||||||
|
webview.settings.userAgentString = request.header("User-Agent") ?: networkHelper.defaultUserAgent
|
||||||
|
return webview
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import logcat.LogPriority
|
||||||
object WebViewUtil {
|
object WebViewUtil {
|
||||||
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
||||||
|
|
||||||
const val MINIMUM_WEBVIEW_VERSION = 99
|
const val MINIMUM_WEBVIEW_VERSION = 100
|
||||||
|
|
||||||
fun supportsWebView(context: Context): Boolean {
|
fun supportsWebView(context: Context): Boolean {
|
||||||
try {
|
try {
|
||||||
|
|
Loading…
Reference in a new issue