添加Cronet Flavor

pull/1120/head
ag2s20150909 3 years ago
parent 87e279ecf4
commit 7aaf9bdf5e
  1. 18
      app/build.gradle
  2. 1
      app/src/main/java/io/legado/app/help/AppConfig.kt
  3. 10
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  4. 6
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  5. 65
      app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt
  6. 20
      app/src/main/java/io/legado/app/help/http/cronet/CronetInterceptor.kt
  7. 207
      app/src/main/java/io/legado/app/help/http/cronet/CronetUrlRequestCallback.kt
  8. 2
      build.gradle

@ -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…
Cancel
Save