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"
|
applicationId "io.legado.play"
|
||||||
manifestPlaceholders = [APP_CHANNEL_VALUE: "google"]
|
manifestPlaceholders = [APP_CHANNEL_VALUE: "google"]
|
||||||
}
|
}
|
||||||
|
cronet {
|
||||||
|
dimension "mode"
|
||||||
|
applicationId "io.legado.cronet"
|
||||||
|
manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
// Flag to enable support for the new language APIs
|
// Flag to enable support for the new language APIs
|
||||||
@ -117,8 +122,8 @@ dependencies {
|
|||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
|
||||||
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
|
implementation fileTree(dir: 'libs', include: ['*.jar','*.aar'])
|
||||||
testImplementation 'junit:junit:4.13.2'
|
testImplementation 'junit:junit:4.13.2'
|
||||||
androidTestImplementation 'androidx.test:runner:1.3.0'
|
androidTestImplementation 'androidx.test:runner:1.4.0'
|
||||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
|
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||||
implementation 'androidx.multidex:multidex:2.0.1'
|
implementation 'androidx.multidex:multidex:2.0.1'
|
||||||
//kotlin
|
//kotlin
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
@ -129,7 +134,7 @@ dependencies {
|
|||||||
|
|
||||||
//androidX
|
//androidX
|
||||||
implementation 'androidx.appcompat:appcompat:1.3.0'
|
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.activity:activity-ktx:1.2.3"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.3.5"
|
implementation "androidx.fragment:fragment-ktx:1.3.5"
|
||||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||||
@ -170,7 +175,7 @@ dependencies {
|
|||||||
//规则相关
|
//规则相关
|
||||||
implementation 'org.jsoup:jsoup:1.14.1'
|
implementation 'org.jsoup:jsoup:1.14.1'
|
||||||
//noinspection GradleDependency
|
//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'
|
implementation 'com.jayway.jsonpath:json-path:2.6.0'
|
||||||
|
|
||||||
//JS rhino
|
//JS rhino
|
||||||
@ -178,6 +183,11 @@ dependencies {
|
|||||||
|
|
||||||
//网络
|
//网络
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
|
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
|
//Glide
|
||||||
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
implementation 'com.github.bumptech.glide:glide:4.12.0'
|
||||||
|
@ -10,6 +10,7 @@ import splitties.init.appCtx
|
|||||||
@Suppress("MemberVisibilityCanBePrivate")
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
val isGooglePlay = appCtx.channel == "google"
|
val isGooglePlay = appCtx.channel == "google"
|
||||||
|
val isCronet= appCtx.channel=="cronet"
|
||||||
var userAgent: String = getPrefUserAgent()
|
var userAgent: String = getPrefUserAgent()
|
||||||
var isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
|
var isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
|
||||||
var clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2)
|
var clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2)
|
||||||
|
@ -292,7 +292,7 @@ interface JsExtensions {
|
|||||||
val bos = ByteArrayOutputStream()
|
val bos = ByteArrayOutputStream()
|
||||||
val zis = ZipInputStream(ByteArrayInputStream(bytes))
|
val zis = ZipInputStream(ByteArrayInputStream(bytes))
|
||||||
|
|
||||||
var entry: ZipEntry = zis.nextEntry
|
var entry: ZipEntry? = zis.nextEntry
|
||||||
|
|
||||||
while (entry != null) {
|
while (entry != null) {
|
||||||
if (entry.name.equals(path)) {
|
if (entry.name.equals(path)) {
|
||||||
@ -303,7 +303,7 @@ interface JsExtensions {
|
|||||||
}
|
}
|
||||||
Debug.log("getZipContent 未发现内容")
|
Debug.log("getZipContent 未发现内容")
|
||||||
|
|
||||||
return "";
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -319,17 +319,17 @@ interface JsExtensions {
|
|||||||
val bos = ByteArrayOutputStream()
|
val bos = ByteArrayOutputStream()
|
||||||
val zis = ZipInputStream(ByteArrayInputStream(bytes))
|
val zis = ZipInputStream(ByteArrayInputStream(bytes))
|
||||||
|
|
||||||
var entry: ZipEntry = zis.nextEntry
|
var entry: ZipEntry? = zis.nextEntry
|
||||||
while (entry != null) {
|
while (entry != null) {
|
||||||
if (entry.name.equals(path)) {
|
if (entry.name.equals(path)) {
|
||||||
zis.use { it.copyTo(bos) }
|
zis.use { it.copyTo(bos) }
|
||||||
return bos.toByteArray()
|
return bos.toByteArray()
|
||||||
}
|
}
|
||||||
entry = zis.nextEntry;
|
entry = zis.nextEntry
|
||||||
}
|
}
|
||||||
Debug.log("getZipContent 未发现内容")
|
Debug.log("getZipContent 未发现内容")
|
||||||
|
|
||||||
return null;
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package io.legado.app.help.http
|
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 kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import okhttp3.ConnectionSpec
|
import okhttp3.ConnectionSpec
|
||||||
import okhttp3.Credentials
|
import okhttp3.Credentials
|
||||||
@ -41,6 +43,10 @@ val okHttpClient: OkHttpClient by lazy {
|
|||||||
.build()
|
.build()
|
||||||
chain.proceed(request)
|
chain.proceed(request)
|
||||||
})
|
})
|
||||||
|
if (AppConfig.isCronet){
|
||||||
|
builder.addInterceptor(CronetInterceptor())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
builder.build()
|
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.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.5.20'
|
ext.kotlin_version = '1.5.21'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
Loading…
Reference in New Issue
Block a user