mirror of
https://github.com/gedoor/legado.git
synced 2024-09-01 09:34:25 +08:00
添加Cronet Flavor
This commit is contained in:
parent
87e279ecf4
commit
7aaf9bdf5e
@ -84,6 +84,11 @@ android {
|
||||
applicationId "io.legado.play"
|
||||
manifestPlaceholders = [APP_CHANNEL_VALUE: "google"]
|
||||
}
|
||||
cronet {
|
||||
dimension "mode"
|
||||
applicationId "io.legado.cronet"
|
||||
manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"]
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
// Flag to enable support for the new language APIs
|
||||
@ -117,8 +122,8 @@ dependencies {
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
//kotlin
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
@ -129,7 +134,7 @@ dependencies {
|
||||
|
||||
//androidX
|
||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
||||
implementation 'androidx.core:core-ktx:1.5.0'
|
||||
implementation 'androidx.core:core-ktx:1.6.0'
|
||||
implementation "androidx.activity:activity-ktx:1.2.3"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.5"
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
@ -170,7 +175,7 @@ dependencies {
|
||||
//规则相关
|
||||
implementation 'org.jsoup:jsoup:1.14.1'
|
||||
//noinspection GradleDependency
|
||||
implementation 'cn.wanghaomiao:JsoupXpath:2.3.2'
|
||||
implementation 'cn.wanghaomiao:JsoupXpath:2.4.3'
|
||||
implementation 'com.jayway.jsonpath:json-path:2.6.0'
|
||||
|
||||
//JS rhino
|
||||
@ -178,6 +183,11 @@ dependencies {
|
||||
|
||||
//网络
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
|
||||
def cronet_version='92.0.4509.1'
|
||||
compileOnly "org.microg:cronet-api:$cronet_version"
|
||||
cronetImplementation "org.microg:cronet-native:$cronet_version"
|
||||
cronetImplementation "org.microg:cronet-api:$cronet_version"
|
||||
cronetImplementation "org.microg:cronet-common:$cronet_version"
|
||||
|
||||
//Glide
|
||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||
|
@ -10,6 +10,7 @@ import splitties.init.appCtx
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
val isGooglePlay = appCtx.channel == "google"
|
||||
val isCronet= appCtx.channel=="cronet"
|
||||
var userAgent: String = getPrefUserAgent()
|
||||
var isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
|
||||
var clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2)
|
||||
|
@ -292,7 +292,7 @@ interface JsExtensions {
|
||||
val bos = ByteArrayOutputStream()
|
||||
val zis = ZipInputStream(ByteArrayInputStream(bytes))
|
||||
|
||||
var entry: ZipEntry = zis.nextEntry
|
||||
var entry: ZipEntry? = zis.nextEntry
|
||||
|
||||
while (entry != null) {
|
||||
if (entry.name.equals(path)) {
|
||||
@ -303,7 +303,7 @@ interface JsExtensions {
|
||||
}
|
||||
Debug.log("getZipContent 未发现内容")
|
||||
|
||||
return "";
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
@ -319,17 +319,17 @@ interface JsExtensions {
|
||||
val bos = ByteArrayOutputStream()
|
||||
val zis = ZipInputStream(ByteArrayInputStream(bytes))
|
||||
|
||||
var entry: ZipEntry = zis.nextEntry
|
||||
var entry: ZipEntry? = zis.nextEntry
|
||||
while (entry != null) {
|
||||
if (entry.name.equals(path)) {
|
||||
zis.use { it.copyTo(bos) }
|
||||
return bos.toByteArray()
|
||||
}
|
||||
entry = zis.nextEntry;
|
||||
entry = zis.nextEntry
|
||||
}
|
||||
Debug.log("getZipContent 未发现内容")
|
||||
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,7 @@
|
||||
package io.legado.app.help.http
|
||||
|
||||
import io.legado.app.help.AppConfig
|
||||
import io.legado.app.help.http.cronet.CronetInterceptor
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import okhttp3.ConnectionSpec
|
||||
import okhttp3.Credentials
|
||||
@ -41,6 +43,10 @@ val okHttpClient: OkHttpClient by lazy {
|
||||
.build()
|
||||
chain.proceed(request)
|
||||
})
|
||||
if (AppConfig.isCronet){
|
||||
builder.addInterceptor(CronetInterceptor())
|
||||
}
|
||||
|
||||
|
||||
builder.build()
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import io.legado.app.help.http.CookieStore
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import okio.Buffer
|
||||
import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK
|
||||
import org.chromium.net.ExperimentalCronetEngine
|
||||
import org.chromium.net.UploadDataProviders
|
||||
import org.chromium.net.UrlRequest
|
||||
import splitties.init.appCtx
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
val executor: Executor by lazy { Executors.newSingleThreadExecutor() }
|
||||
|
||||
val cronetEngine: ExperimentalCronetEngine by lazy {
|
||||
|
||||
val builder = ExperimentalCronetEngine.Builder(appCtx)
|
||||
.setStoragePath(appCtx.externalCacheDir?.absolutePath)
|
||||
.enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50))
|
||||
.enableQuic(true)
|
||||
.enablePublicKeyPinningBypassForLocalTrustAnchors(true)
|
||||
.enableHttp2(true)
|
||||
//Brotli压缩
|
||||
builder.enableBrotli(true)
|
||||
return@lazy builder.build()
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun buildRequest(request: Request, callback: UrlRequest.Callback): UrlRequest {
|
||||
val url = request.url.toString()
|
||||
val requestBuilder = cronetEngine.newUrlRequestBuilder(url, callback, executor)
|
||||
requestBuilder.setHttpMethod(request.method)
|
||||
val cookie = CookieStore.getCookie(url)
|
||||
if (cookie.length > 1) {
|
||||
requestBuilder.addHeader("Cookie", cookie)
|
||||
}
|
||||
val headers: Headers = request.headers
|
||||
headers.forEachIndexed { index, pair ->
|
||||
requestBuilder.addHeader(headers.name(index), headers.value(index))
|
||||
}
|
||||
|
||||
val requestBody = request.body
|
||||
if (requestBody != null) {
|
||||
val contentType: MediaType? = requestBody.contentType()
|
||||
if (contentType != null) {
|
||||
requestBuilder.addHeader("Content-Type", contentType.toString())
|
||||
}
|
||||
val buffer = Buffer()
|
||||
requestBody.writeTo(buffer)
|
||||
requestBuilder.setUploadDataProvider(
|
||||
UploadDataProviders.create(buffer.readByteArray()),
|
||||
executor
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
return requestBuilder.build()
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import okhttp3.*
|
||||
import java.io.IOException
|
||||
|
||||
class CronetInterceptor : Interceptor {
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
return proceedWithCronet(chain.request(), chain.call())
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun proceedWithCronet(request: Request, call: Call): Response {
|
||||
val callback = CronetUrlRequestCallback(request, call)
|
||||
val urlRequest = buildRequest(request, callback)
|
||||
urlRequest.start()
|
||||
return callback.waitForDone()
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import android.os.ConditionVariable
|
||||
import android.util.Log
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import okhttp3.*
|
||||
import okhttp3.EventListener
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.chromium.net.CronetException
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.Channels
|
||||
import java.util.*
|
||||
|
||||
class CronetUrlRequestCallback @JvmOverloads internal constructor(
|
||||
private val originalRequest: Request,
|
||||
private val mCall: Call,
|
||||
eventListener: EventListener? = null,
|
||||
responseCallback: Callback? = null
|
||||
) : UrlRequest.Callback() {
|
||||
private val eventListener: EventListener?
|
||||
private val responseCallback: Callback?
|
||||
private var followCount = 0
|
||||
private var mResponse: Response
|
||||
private var mException: IOException? = null
|
||||
private val mResponseCondition = ConditionVariable()
|
||||
private val mBytesReceived = ByteArrayOutputStream()
|
||||
private val mReceiveChannel = Channels.newChannel(mBytesReceived)
|
||||
|
||||
@Throws(IOException::class)
|
||||
fun waitForDone(): Response {
|
||||
mResponseCondition.block()
|
||||
if (mException != null) {
|
||||
throw mException as IOException
|
||||
}
|
||||
return mResponse
|
||||
}
|
||||
|
||||
override fun onRedirectReceived(
|
||||
request: UrlRequest,
|
||||
info: UrlResponseInfo,
|
||||
newLocationUrl: String
|
||||
) {
|
||||
if (followCount > MAX_FOLLOW_COUNT) {
|
||||
request.cancel()
|
||||
}
|
||||
followCount += 1
|
||||
val client = okHttpClient
|
||||
if (originalRequest.url.isHttps && newLocationUrl.startsWith("http://") && client.followSslRedirects) {
|
||||
request.followRedirect()
|
||||
} else if (!originalRequest.url.isHttps && newLocationUrl.startsWith("https://") && client.followSslRedirects) {
|
||||
request.followRedirect()
|
||||
} else if (client.followRedirects) {
|
||||
request.followRedirect()
|
||||
} else {
|
||||
request.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
|
||||
mResponse = responseFromResponse(mResponse, info)
|
||||
// val sb: StringBuilder = StringBuilder(info.url).append("\r\n")
|
||||
// sb.append("[Cached:").append(info.wasCached()).append("][StatusCode:")
|
||||
// .append(info.httpStatusCode).append("][StatusText:").append(info.httpStatusText)
|
||||
// .append("][Protocol:").append(info.negotiatedProtocol).append("][ByteCount:")
|
||||
// .append(info.receivedByteCount).append("]\r\n");
|
||||
// val httpHeaders=info.allHeadersAsList
|
||||
// httpHeaders.forEach { h ->
|
||||
// sb.append("[").append(h.key).append("]").append(h.value).append("\r\n");
|
||||
// }
|
||||
// Log.e("Cronet", sb.toString())
|
||||
if (eventListener != null) {
|
||||
eventListener.responseHeadersEnd(mCall, mResponse)
|
||||
eventListener.responseBodyStart(mCall)
|
||||
}
|
||||
request.read(ByteBuffer.allocateDirect(32 * 1024))
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun onReadCompleted(
|
||||
request: UrlRequest,
|
||||
info: UrlResponseInfo,
|
||||
byteBuffer: ByteBuffer
|
||||
) {
|
||||
byteBuffer.flip()
|
||||
try {
|
||||
mReceiveChannel.write(byteBuffer)
|
||||
} catch (e: IOException) {
|
||||
Log.i(TAG, "IOException during ByteBuffer read. Details: ", e)
|
||||
throw e
|
||||
}
|
||||
byteBuffer.clear()
|
||||
request.read(byteBuffer)
|
||||
}
|
||||
|
||||
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
||||
eventListener?.responseBodyEnd(mCall, info.receivedByteCount)
|
||||
val contentType: MediaType? = (mResponse.header("content-type")
|
||||
?: "text/plain; charset=\"utf-8\"").toMediaTypeOrNull()
|
||||
val responseBody: ResponseBody =
|
||||
mBytesReceived.toByteArray().toResponseBody(contentType)
|
||||
val newRequest = originalRequest.newBuilder().url(info.url).build()
|
||||
mResponse = mResponse.newBuilder().body(responseBody).request(newRequest).build()
|
||||
mResponseCondition.open()
|
||||
eventListener?.callEnd(mCall)
|
||||
if (responseCallback != null) {
|
||||
try {
|
||||
responseCallback.onResponse(mCall, mResponse)
|
||||
} catch (e: IOException) {
|
||||
// Pass?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailed(request: UrlRequest, info: UrlResponseInfo, error: CronetException) {
|
||||
val e = IOException("Cronet Exception Occurred", error)
|
||||
mException = e
|
||||
mResponseCondition.open()
|
||||
eventListener?.callFailed(mCall, e)
|
||||
responseCallback?.onFailure(mCall, e)
|
||||
}
|
||||
|
||||
override fun onCanceled(request: UrlRequest, info: UrlResponseInfo) {
|
||||
mResponseCondition.open()
|
||||
eventListener?.callEnd(mCall)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "Callback"
|
||||
private const val MAX_FOLLOW_COUNT = 20
|
||||
private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol {
|
||||
val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault())
|
||||
// Log.e("Cronet", responseInfo.url)
|
||||
// Log.e("Cronet", negotiatedProtocol)
|
||||
|
||||
return when {
|
||||
negotiatedProtocol.contains("h3") -> {
|
||||
return Protocol.QUIC
|
||||
}
|
||||
negotiatedProtocol.contains("quic") -> {
|
||||
Protocol.QUIC
|
||||
}
|
||||
negotiatedProtocol.contains("spdy") -> {
|
||||
Protocol.SPDY_3
|
||||
}
|
||||
negotiatedProtocol.contains("h2") -> {
|
||||
Protocol.HTTP_2
|
||||
}
|
||||
negotiatedProtocol.contains("1.1") -> {
|
||||
Protocol.HTTP_1_1
|
||||
}
|
||||
else -> {
|
||||
Protocol.HTTP_1_0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun headersFromResponse(responseInfo: UrlResponseInfo): Headers {
|
||||
val headers = responseInfo.allHeadersAsList
|
||||
val headerBuilder = Headers.Builder()
|
||||
for ((key, value) in headers) {
|
||||
try {
|
||||
if (key.equals("content-encoding", ignoreCase = true)) {
|
||||
// Strip all content encoding headers as decoding is done handled by cronet
|
||||
continue
|
||||
}
|
||||
headerBuilder.add(key, value)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Invalid HTTP header/value: $key$value")
|
||||
// Ignore that header
|
||||
}
|
||||
}
|
||||
return headerBuilder.build()
|
||||
}
|
||||
|
||||
private fun responseFromResponse(
|
||||
response: Response,
|
||||
responseInfo: UrlResponseInfo
|
||||
): Response {
|
||||
val protocol = protocolFromNegotiatedProtocol(responseInfo)
|
||||
val headers = headersFromResponse(responseInfo)
|
||||
return response.newBuilder()
|
||||
.receivedResponseAtMillis(System.currentTimeMillis())
|
||||
.protocol(protocol)
|
||||
.code(responseInfo.httpStatusCode)
|
||||
.message(responseInfo.httpStatusText)
|
||||
.headers(headers)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
mResponse = Response.Builder()
|
||||
.sentRequestAtMillis(System.currentTimeMillis())
|
||||
.request(originalRequest)
|
||||
.protocol(Protocol.HTTP_1_0)
|
||||
.code(0)
|
||||
.message("")
|
||||
.build()
|
||||
this.responseCallback = responseCallback
|
||||
this.eventListener = eventListener
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.5.20'
|
||||
ext.kotlin_version = '1.5.21'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
|
Loading…
Reference in New Issue
Block a user