优化
This commit is contained in:
@@ -0,0 +1,484 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.http.CookieManager
|
||||
import io.legado.app.help.http.CookieManager.cookieJarHeader
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.utils.DebugLog
|
||||
import io.legado.app.utils.asIOException
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.*
|
||||
import okhttp3.EventListener
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||
import okhttp3.internal.http.HttpMethod
|
||||
import okhttp3.internal.http.StatusLine
|
||||
import okio.Buffer
|
||||
import okio.Source
|
||||
import okio.Timeout
|
||||
import okio.buffer
|
||||
import org.chromium.net.CronetException
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
import java.io.IOException
|
||||
import java.net.ProtocolException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
import java.util.concurrent.ArrayBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
|
||||
@Keep
|
||||
abstract class AbsCallBack(
|
||||
var originalRequest: Request,
|
||||
val mCall: Call,
|
||||
private val eventListener: EventListener? = null,
|
||||
private val responseCallback: Callback? = null
|
||||
) : UrlRequest.Callback() {
|
||||
|
||||
var mResponse: Response
|
||||
private var followCount = 0
|
||||
private var request: UrlRequest? = null
|
||||
private var finished = AtomicBoolean(false)
|
||||
private val canceled = AtomicBoolean(false)
|
||||
private val callbackResults = ArrayBlockingQueue<CallbackResult>(2)
|
||||
private val urlResponseInfoChain = arrayListOf<UrlResponseInfo>()
|
||||
private var cancelJob: Coroutine<*>? = null
|
||||
private var followRedirect = false
|
||||
private var enableCookieJar = false
|
||||
private var redirectRequest: Request? = null
|
||||
|
||||
init {
|
||||
if (originalRequest.header(cookieJarHeader) != null) {
|
||||
enableCookieJar = true
|
||||
originalRequest = originalRequest.newBuilder()
|
||||
.removeHeader(cookieJarHeader).build()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
abstract fun waitForDone(urlRequest: UrlRequest): Response
|
||||
|
||||
/**
|
||||
* 当发生错误时,通知子类终止阻塞抛出错误
|
||||
* @param error
|
||||
*/
|
||||
abstract fun onError(error: IOException)
|
||||
|
||||
/**
|
||||
* 请求成功后,通知子类结束阻塞,返回response
|
||||
* @param response
|
||||
*/
|
||||
abstract fun onSuccess(response: Response)
|
||||
|
||||
|
||||
override fun onRedirectReceived(
|
||||
request: UrlRequest,
|
||||
info: UrlResponseInfo,
|
||||
newLocationUrl: String
|
||||
) {
|
||||
if (followCount > MAX_FOLLOW_COUNT) {
|
||||
request.cancel()
|
||||
onError(IOException("Too many redirect"))
|
||||
return
|
||||
}
|
||||
if (mCall.isCanceled()) {
|
||||
onError(IOException("Request Canceled"))
|
||||
request.cancel()
|
||||
return
|
||||
}
|
||||
followCount += 1
|
||||
urlResponseInfoChain.add(info)
|
||||
val client = okHttpClient
|
||||
if (originalRequest.url.isHttps && newLocationUrl.startsWith("http://") && client.followSslRedirects) {
|
||||
followRedirect = true
|
||||
} else if (!originalRequest.url.isHttps && newLocationUrl.startsWith("https://") && client.followSslRedirects) {
|
||||
followRedirect = true
|
||||
} else if (okHttpClient.followRedirects) {
|
||||
followRedirect = true
|
||||
}
|
||||
|
||||
if (!followRedirect) {
|
||||
onError(IOException("Too many redirect"))
|
||||
} else {
|
||||
val response = toResponse(originalRequest, info, urlResponseInfoChain)
|
||||
if (enableCookieJar) {
|
||||
CookieManager.saveResponse(response)
|
||||
}
|
||||
redirectRequest = buildRedirectRequest(response, originalRequest.method, newLocationUrl)
|
||||
}
|
||||
request.cancel()
|
||||
}
|
||||
|
||||
|
||||
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
|
||||
this.request = request
|
||||
|
||||
cancelJob = Coroutine.async {
|
||||
while (!mCall.isCanceled()) {
|
||||
delay(1000)
|
||||
}
|
||||
request.cancel()
|
||||
}
|
||||
|
||||
val response: Response
|
||||
try {
|
||||
response = toResponse(originalRequest, info, urlResponseInfoChain, CronetBodySource())
|
||||
} catch (e: IOException) {
|
||||
request.cancel()
|
||||
cancelJob?.cancel()
|
||||
onError(e)
|
||||
return
|
||||
}
|
||||
|
||||
if (enableCookieJar) {
|
||||
CookieManager.saveResponse(response)
|
||||
}
|
||||
|
||||
mResponse = response
|
||||
onSuccess(response)
|
||||
|
||||
//打印协议,用于调试
|
||||
val msg = "onResponseStarted[${info.negotiatedProtocol}][${info.httpStatusCode}]${info.url}"
|
||||
DebugLog.i(javaClass.simpleName, msg)
|
||||
if (eventListener != null) {
|
||||
eventListener.responseHeadersEnd(mCall, response)
|
||||
eventListener.responseBodyStart(mCall)
|
||||
}
|
||||
try {
|
||||
responseCallback?.onResponse(mCall, response)
|
||||
} catch (e: IOException) {
|
||||
// Pass?
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onReadCompleted(
|
||||
request: UrlRequest,
|
||||
info: UrlResponseInfo,
|
||||
byteBuffer: ByteBuffer
|
||||
) {
|
||||
callbackResults.add(CallbackResult(CallbackStep.ON_READ_COMPLETED, byteBuffer))
|
||||
}
|
||||
|
||||
|
||||
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
||||
callbackResults.add(CallbackResult(CallbackStep.ON_SUCCESS))
|
||||
cancelJob?.cancel()
|
||||
eventListener?.responseBodyEnd(mCall, info.receivedByteCount)
|
||||
//DebugLog.i(javaClass.simpleName, "end[${info.negotiatedProtocol}]${info.url}")
|
||||
|
||||
eventListener?.callEnd(mCall)
|
||||
}
|
||||
|
||||
|
||||
//UrlResponseInfo可能为null
|
||||
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
|
||||
callbackResults.add(CallbackResult(CallbackStep.ON_FAILED, null, error))
|
||||
DebugLog.e(javaClass.name, error.message.toString())
|
||||
onError(error.asIOException())
|
||||
eventListener?.callFailed(mCall, error)
|
||||
responseCallback?.onFailure(mCall, error)
|
||||
}
|
||||
|
||||
override fun onCanceled(request: UrlRequest?, info: UrlResponseInfo?) {
|
||||
if (followRedirect) {
|
||||
followRedirect = false
|
||||
if (enableCookieJar) {
|
||||
val newRequest = CookieManager.loadRequest(redirectRequest!!)
|
||||
buildRequest(newRequest, this)?.start()
|
||||
} else {
|
||||
buildRequest(redirectRequest!!, this)?.start()
|
||||
}
|
||||
return
|
||||
}
|
||||
canceled.set(true)
|
||||
callbackResults.add(CallbackResult(CallbackStep.ON_CANCELED))
|
||||
//DebugLog.i(javaClass.simpleName, "cancel[${info?.negotiatedProtocol}]${info?.url}")
|
||||
eventListener?.callEnd(mCall)
|
||||
//onError(IOException("Cronet Request Canceled"))
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
mResponse = Response.Builder()
|
||||
.sentRequestAtMillis(System.currentTimeMillis())
|
||||
.request(originalRequest)
|
||||
.protocol(Protocol.HTTP_1_0)
|
||||
.code(0)
|
||||
.message("")
|
||||
.build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_FOLLOW_COUNT = 20
|
||||
private val encodingsHandledByCronet = setOf("br", "deflate", "gzip", "x-gzip")
|
||||
|
||||
private fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol {
|
||||
val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault())
|
||||
return when {
|
||||
negotiatedProtocol.contains("h3") -> {
|
||||
Protocol.QUIC
|
||||
}
|
||||
|
||||
negotiatedProtocol.contains("quic") -> {
|
||||
Protocol.QUIC
|
||||
}
|
||||
|
||||
negotiatedProtocol.contains("spdy") -> {
|
||||
@Suppress("DEPRECATION")
|
||||
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,
|
||||
keepEncodingAffectedHeaders: Boolean
|
||||
): Headers {
|
||||
|
||||
val headers = responseInfo.allHeadersAsList
|
||||
return Headers.Builder().apply {
|
||||
for ((key, value) in headers) {
|
||||
try {
|
||||
|
||||
if (!keepEncodingAffectedHeaders
|
||||
&& (key.equals("content-encoding", ignoreCase = true)
|
||||
|| key.equals("Content-Length", ignoreCase = true))
|
||||
) {
|
||||
// Strip all content encoding headers as decoding is done handled by cronet
|
||||
continue
|
||||
}
|
||||
add(key, value)
|
||||
} catch (e: Exception) {
|
||||
DebugLog.w(javaClass.name, "Invalid HTTP header/value: $key$value")
|
||||
// Ignore that header
|
||||
}
|
||||
}
|
||||
|
||||
}.build()
|
||||
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun createResponse(
|
||||
request: Request,
|
||||
responseInfo: UrlResponseInfo,
|
||||
bodySource: Source? = null
|
||||
): Response.Builder {
|
||||
val protocol = protocolFromNegotiatedProtocol(responseInfo)
|
||||
|
||||
val contentEncodingHeaders =
|
||||
responseInfo.allHeaders.getOrDefault("content-encoding", emptyList())
|
||||
val contentEncodingItems = contentEncodingHeaders.flatMap {
|
||||
it.splitNotBlank(",").toList()
|
||||
}
|
||||
val keepEncodingAffectedHeaders = contentEncodingItems.isEmpty()
|
||||
|| !encodingsHandledByCronet.containsAll(contentEncodingItems)
|
||||
|
||||
val headers = headersFromResponse(responseInfo, keepEncodingAffectedHeaders)
|
||||
val contentLength = if (keepEncodingAffectedHeaders) {
|
||||
responseInfo.allHeaders["Content-Length"]?.lastOrNull()
|
||||
} else null
|
||||
val contentType = responseInfo.allHeaders["content-type"]?.lastOrNull()
|
||||
?: "text/plain; charset=\"utf-8\""
|
||||
|
||||
val responseBody = bodySource?.let {
|
||||
createResponseBody(
|
||||
request,
|
||||
responseInfo.httpStatusCode,
|
||||
contentType,
|
||||
contentLength,
|
||||
bodySource
|
||||
)
|
||||
}
|
||||
|
||||
return Response.Builder()
|
||||
.request(request)
|
||||
.receivedResponseAtMillis(System.currentTimeMillis())
|
||||
.protocol(protocol)
|
||||
.code(responseInfo.httpStatusCode)
|
||||
.message(responseInfo.httpStatusText)
|
||||
.headers(headers)
|
||||
.body(responseBody)
|
||||
}
|
||||
|
||||
private fun buildPriorResponse(
|
||||
request: Request,
|
||||
redirectResponseInfos: List<UrlResponseInfo>,
|
||||
): Response? {
|
||||
var priorResponse: Response? = null
|
||||
if (redirectResponseInfos.isNotEmpty()) {
|
||||
for (i in redirectResponseInfos.indices) {
|
||||
val url = redirectResponseInfos[i].url
|
||||
val redirectedRequest = request.newBuilder().url(url).build()
|
||||
priorResponse = createResponse(redirectedRequest, redirectResponseInfos[i])
|
||||
.priorResponse(priorResponse)
|
||||
.build()
|
||||
}
|
||||
|
||||
}
|
||||
return priorResponse
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun createResponseBody(
|
||||
request: Request,
|
||||
httpStatusCode: Int,
|
||||
contentType: String?,
|
||||
contentLengthString: String?,
|
||||
bodySource: Source
|
||||
): ResponseBody {
|
||||
|
||||
// Ignore content-length header for HEAD requests (consistency with OkHttp)
|
||||
val contentLength: Long = if (request.method == "HEAD") {
|
||||
0
|
||||
} else {
|
||||
contentLengthString?.toLongOrNull() ?: -1
|
||||
}
|
||||
|
||||
// Check for absence of body in No Content / Reset Content responses (OkHttp consistency)
|
||||
if ((httpStatusCode == 204 || httpStatusCode == 205) && contentLength > 0) {
|
||||
throw ProtocolException(
|
||||
"HTTP $httpStatusCode had non-zero Content-Length: $contentLengthString"
|
||||
)
|
||||
}
|
||||
return bodySource.buffer()
|
||||
.asResponseBody(contentType?.toMediaTypeOrNull(), contentLength)
|
||||
}
|
||||
|
||||
private fun buildRedirectRequest(
|
||||
userResponse: Response,
|
||||
method: String,
|
||||
newLocationUrl: String
|
||||
): Request {
|
||||
// Most redirects don't include a request body.
|
||||
val requestBuilder = userResponse.request.newBuilder()
|
||||
if (HttpMethod.permitsRequestBody(method)) {
|
||||
val responseCode = userResponse.code
|
||||
val maintainBody = HttpMethod.redirectsWithBody(method) ||
|
||||
responseCode == StatusLine.HTTP_PERM_REDIRECT ||
|
||||
responseCode == StatusLine.HTTP_TEMP_REDIRECT
|
||||
if (HttpMethod.redirectsToGet(method) && responseCode != StatusLine.HTTP_PERM_REDIRECT && responseCode != StatusLine.HTTP_TEMP_REDIRECT) {
|
||||
requestBuilder.method("GET", null)
|
||||
} else {
|
||||
val requestBody = if (maintainBody) userResponse.request.body else null
|
||||
requestBuilder.method(method, requestBody)
|
||||
}
|
||||
if (!maintainBody) {
|
||||
requestBuilder.removeHeader("Transfer-Encoding")
|
||||
requestBuilder.removeHeader("Content-Length")
|
||||
requestBuilder.removeHeader("Content-Type")
|
||||
}
|
||||
}
|
||||
|
||||
return requestBuilder.url(newLocationUrl).build()
|
||||
}
|
||||
|
||||
private fun toResponse(
|
||||
request: Request,
|
||||
responseInfo: UrlResponseInfo,
|
||||
redirectResponseInfos: List<UrlResponseInfo>,
|
||||
bodySource: Source? = null
|
||||
): Response {
|
||||
val responseBuilder = createResponse(request, responseInfo, bodySource)
|
||||
val newRequest = request.newBuilder().url(responseInfo.url).build()
|
||||
return responseBuilder
|
||||
.request(newRequest)
|
||||
.priorResponse(buildPriorResponse(request, redirectResponseInfos))
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
inner class CronetBodySource : Source {
|
||||
|
||||
private var buffer = ByteBuffer.allocateDirect(32 * 1024)
|
||||
private var closed = false
|
||||
private val timeout = mCall.timeout().timeoutNanos()
|
||||
|
||||
override fun close() {
|
||||
cancelJob?.cancel()
|
||||
if (closed) {
|
||||
return
|
||||
}
|
||||
closed = true
|
||||
if (!finished.get()) {
|
||||
request?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
if (canceled.get()) {
|
||||
throw IOException("Request Canceled")
|
||||
}
|
||||
|
||||
require(byteCount >= 0L) { "byteCount < 0: $byteCount" }
|
||||
check(!closed) { "closed" }
|
||||
|
||||
if (finished.get()) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (byteCount < buffer.limit()) {
|
||||
buffer.limit(byteCount.toInt())
|
||||
}
|
||||
|
||||
request?.read(buffer)
|
||||
|
||||
val result = callbackResults.poll(timeout, TimeUnit.NANOSECONDS)
|
||||
if (result == null) {
|
||||
request?.cancel()
|
||||
throw IOException("Body Read Timeout")
|
||||
}
|
||||
|
||||
return when (result.callbackStep) {
|
||||
CallbackStep.ON_FAILED -> {
|
||||
finished.set(true)
|
||||
buffer = null
|
||||
throw IOException(result.exception)
|
||||
}
|
||||
|
||||
CallbackStep.ON_SUCCESS -> {
|
||||
finished.set(true)
|
||||
buffer = null
|
||||
-1
|
||||
}
|
||||
|
||||
CallbackStep.ON_CANCELED -> {
|
||||
buffer = null
|
||||
throw IOException("Request Canceled")
|
||||
}
|
||||
|
||||
CallbackStep.ON_READ_COMPLETED -> {
|
||||
result.buffer!!.flip()
|
||||
val bytesWritten = sink.write(result.buffer)
|
||||
result.buffer.clear()
|
||||
bytesWritten.toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun timeout(): Timeout {
|
||||
return mCall.timeout()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import okhttp3.RequestBody
|
||||
import okio.Buffer
|
||||
import org.chromium.net.UploadDataProvider
|
||||
import org.chromium.net.UploadDataSink
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@Keep
|
||||
class BodyUploadProvider(private val body: RequestBody) : UploadDataProvider(), AutoCloseable {
|
||||
|
||||
private val buffer = Buffer()
|
||||
|
||||
@Volatile
|
||||
private var filled: Boolean = false
|
||||
|
||||
init {
|
||||
fillBuffer()
|
||||
}
|
||||
|
||||
private fun fillBuffer() {
|
||||
try {
|
||||
buffer.clear()
|
||||
filled = true
|
||||
body.writeTo(buffer)
|
||||
buffer.flush()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getLength(): Long {
|
||||
return body.contentLength()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
|
||||
if (!filled) {
|
||||
fillBuffer()
|
||||
}
|
||||
check(byteBuffer.hasRemaining()) { "Cronet passed a buffer with no bytes remaining" }
|
||||
var read: Int
|
||||
var bytesRead = 0
|
||||
while (bytesRead == 0) {
|
||||
read = buffer.read(byteBuffer)
|
||||
bytesRead += read
|
||||
}
|
||||
uploadDataSink.onReadSucceeded(false)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun rewind(uploadDataSink: UploadDataSink) {
|
||||
check(body.isOneShot()) { "Okhttp RequestBody is oneShot" }
|
||||
filled = false
|
||||
fillBuffer()
|
||||
uploadDataSink.onRewindSucceeded()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
buffer.close()
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import org.chromium.net.CronetException
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
|
||||
data class CallbackResult(
|
||||
val callbackStep: CallbackStep,
|
||||
val buffer: ByteBuffer? = null,
|
||||
val exception: CronetException? = null
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
enum class CallbackStep {
|
||||
ON_READ_COMPLETED,
|
||||
ON_SUCCESS,
|
||||
ON_FAILED,
|
||||
ON_CANCELED
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import io.legado.app.utils.printOnDebug
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import okhttp3.*
|
||||
import okhttp3.internal.http.receiveHeaders
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
import java.io.IOException
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
@Keep
|
||||
@Suppress("unused")
|
||||
class CronetCoroutineInterceptor(private val cookieJar: CookieJar) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException("Canceled")
|
||||
}
|
||||
val original: Request = chain.request()
|
||||
//Cronet未初始化
|
||||
return if (!CronetLoader.install() || cronetEngine == null) {
|
||||
chain.proceed(original)
|
||||
} else try {
|
||||
val builder: Request.Builder = original.newBuilder()
|
||||
//移除Keep-Alive,手动设置会导致400 BadRequest
|
||||
builder.removeHeader("Keep-Alive")
|
||||
builder.removeHeader("Accept-Encoding")
|
||||
if (cookieJar != CookieJar.NO_COOKIES) {
|
||||
val cookieStr = getCookie(original.url)
|
||||
//设置Cookie
|
||||
if (cookieStr.length > 3) {
|
||||
builder.addHeader("Cookie", cookieStr)
|
||||
}
|
||||
}
|
||||
|
||||
val newReq = builder.build()
|
||||
val timeout = chain.call().timeout().timeoutNanos() / 1000000
|
||||
runBlocking() {
|
||||
if (timeout > 0) {
|
||||
withTimeout(timeout) {
|
||||
proceedWithCronet(newReq, chain.call()).also { response ->
|
||||
cookieJar.receiveHeaders(newReq.url, response.headers)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
proceedWithCronet(newReq, chain.call()).also { response ->
|
||||
cookieJar.receiveHeaders(newReq.url, response.headers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
//不能抛出错误,抛出错误会导致应用崩溃
|
||||
//遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理
|
||||
if (!e.message.toString().contains("ERR_CERT_", true)
|
||||
&& !e.message.toString().contains("ERR_SSL_", true)
|
||||
) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
chain.proceed(original)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private suspend fun proceedWithCronet(request: Request, call: Call): Response =
|
||||
suspendCancellableCoroutine<Response> { coroutine ->
|
||||
|
||||
val callBack = object : AbsCallBack(originalRequest = request, mCall = call) {
|
||||
override fun waitForDone(urlRequest: UrlRequest): Response {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onError(error: IOException) {
|
||||
coroutine.resumeWithException(error)
|
||||
}
|
||||
|
||||
override fun onSuccess(response: Response) {
|
||||
coroutine.resume(response)
|
||||
}
|
||||
|
||||
override fun onCanceled(request: UrlRequest?, info: UrlResponseInfo?) {
|
||||
super.onCanceled(request, info)
|
||||
coroutine.cancel()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
val req = buildRequest(request, callBack)?.also { it.start() }
|
||||
coroutine.invokeOnCancellation {
|
||||
req?.cancel()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */
|
||||
private fun getCookie(url: HttpUrl): String = buildString {
|
||||
val cookies = cookieJar.loadForRequest(url)
|
||||
cookies.forEachIndexed { index, cookie ->
|
||||
if (index > 0) append("; ")
|
||||
append(cookie.name).append('=').append(cookie.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
@file:Keep
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.help.http.CookieManager.cookieJarHeader
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.utils.DebugLog
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.Request
|
||||
import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK
|
||||
import org.chromium.net.ExperimentalCronetEngine
|
||||
import org.chromium.net.UploadDataProvider
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.json.JSONObject
|
||||
import splitties.init.appCtx
|
||||
|
||||
internal const val BUFFER_SIZE = 32 * 1024
|
||||
|
||||
val cronetEngine: ExperimentalCronetEngine? by lazy {
|
||||
CronetLoader.preDownload()
|
||||
val builder = ExperimentalCronetEngine.Builder(appCtx).apply {
|
||||
if (CronetLoader.install()) {
|
||||
setLibraryLoader(CronetLoader)//设置自定义so库加载
|
||||
}
|
||||
setStoragePath(appCtx.externalCacheDir?.absolutePath)//设置缓存路径
|
||||
enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50).toLong())//设置50M的磁盘缓存
|
||||
enableQuic(true)//设置支持http/3
|
||||
enableHttp2(true) //设置支持http/2
|
||||
enablePublicKeyPinningBypassForLocalTrustAnchors(true)
|
||||
enableBrotli(true)//Brotli压缩
|
||||
setExperimentalOptions(options)
|
||||
}
|
||||
try {
|
||||
val engine = builder.build()
|
||||
DebugLog.d("Cronet Version:", engine.versionString)
|
||||
return@lazy engine
|
||||
} catch (e: Throwable) {
|
||||
AppLog.put("初始化cronetEngine出错", e)
|
||||
return@lazy null
|
||||
}
|
||||
}
|
||||
|
||||
val options by lazy {
|
||||
val options = JSONObject()
|
||||
|
||||
//设置域名映射规则
|
||||
//MAP hostname ip,MAP hostname ip
|
||||
// val host = JSONObject()
|
||||
// host.put("host_resolver_rules","")
|
||||
// options.put("HostResolverRules", host)
|
||||
|
||||
//启用DnsHttpsSvcb更容易迁移到http3
|
||||
val dnsSvcb = JSONObject()
|
||||
dnsSvcb.put("enable", true)
|
||||
dnsSvcb.put("enable_insecure", true)
|
||||
dnsSvcb.put("use_alpn", true)
|
||||
options.put("UseDnsHttpsSvcb", dnsSvcb)
|
||||
|
||||
options.put("AsyncDNS", JSONObject("{'enable':true}"))
|
||||
|
||||
|
||||
options.toString()
|
||||
}
|
||||
|
||||
fun buildRequest(request: Request, callback: UrlRequest.Callback): UrlRequest? {
|
||||
val url = request.url.toString()
|
||||
val headers: Headers = request.headers
|
||||
val requestBody = request.body
|
||||
return cronetEngine?.newUrlRequestBuilder(
|
||||
url,
|
||||
callback,
|
||||
okHttpClient.dispatcher.executorService
|
||||
)?.apply {
|
||||
setHttpMethod(request.method)//设置
|
||||
allowDirectExecutor()
|
||||
headers.forEachIndexed { index, _ ->
|
||||
if (headers.name(index) == cookieJarHeader) return@forEachIndexed
|
||||
addHeader(headers.name(index), headers.value(index))
|
||||
}
|
||||
if (requestBody != null) {
|
||||
val contentType: MediaType? = requestBody.contentType()
|
||||
if (contentType != null) {
|
||||
addHeader("Content-Type", contentType.toString())
|
||||
} else {
|
||||
addHeader("Content-Type", "text/plain")
|
||||
}
|
||||
val provider: UploadDataProvider = if (requestBody.contentLength() > BUFFER_SIZE) {
|
||||
LargeBodyUploadProvider(requestBody, okHttpClient.dispatcher.executorService)
|
||||
} else {
|
||||
BodyUploadProvider(requestBody)
|
||||
}
|
||||
provider.use {
|
||||
this.setUploadDataProvider(it, okHttpClient.dispatcher.executorService)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}?.build()
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.annotation.Keep
|
||||
import io.legado.app.utils.printOnDebug
|
||||
import okhttp3.*
|
||||
import java.io.IOException
|
||||
|
||||
@Keep
|
||||
@Suppress("unused")
|
||||
class CronetInterceptor(private val cookieJar: CookieJar) : Interceptor {
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
if (chain.call().isCanceled()) {
|
||||
throw IOException("Canceled")
|
||||
}
|
||||
val original: Request = chain.request()
|
||||
//Cronet未初始化
|
||||
return if (!CronetLoader.install() || cronetEngine == null) {
|
||||
chain.proceed(original)
|
||||
} else try {
|
||||
val builder: Request.Builder = original.newBuilder()
|
||||
//移除Keep-Alive,手动设置会导致400 BadRequest
|
||||
builder.removeHeader("Keep-Alive")
|
||||
builder.removeHeader("Accept-Encoding")
|
||||
|
||||
val newReq = builder.build()
|
||||
proceedWithCronet(newReq, chain.call())?.let { response ->
|
||||
//从Response 中保存Cookie到CookieJar
|
||||
//cookieJar.receiveHeaders(newReq.url, response.headers)
|
||||
response
|
||||
} ?: chain.proceed(original)
|
||||
} catch (e: Exception) {
|
||||
//不能抛出错误,抛出错误会导致应用崩溃
|
||||
//遇到Cronet处理有问题时的情况,如证书过期等等,回退到okhttp处理
|
||||
if (!e.message.toString().contains("ERR_CERT_", true)
|
||||
&& !e.message.toString().contains("ERR_SSL_", true)
|
||||
) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
chain.proceed(original)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private fun proceedWithCronet(request: Request, call: Call): Response? {
|
||||
val callBack = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
NewCallBack(request, call)
|
||||
} else {
|
||||
OldCallback(request, call)
|
||||
}
|
||||
buildRequest(request, callBack)?.runCatching {
|
||||
return callBack.waitForDone(this)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
/** Returns a 'Cookie' HTTP request header with all cookies, like `a=b; c=d`. */
|
||||
private fun getCookie(url: HttpUrl): String = buildString {
|
||||
val cookies = cookieJar.loadForRequest(url)
|
||||
cookies.forEachIndexed { index, cookie ->
|
||||
if (index > 0) append("; ")
|
||||
append(cookie.name).append('=').append(cookie.value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import androidx.annotation.Keep
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.http.Cronet
|
||||
import io.legado.app.utils.DebugLog
|
||||
import io.legado.app.utils.printOnDebug
|
||||
import org.chromium.net.CronetEngine
|
||||
import org.json.JSONObject
|
||||
import splitties.init.appCtx
|
||||
import java.io.*
|
||||
import java.math.BigInteger
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
|
||||
@Keep
|
||||
object CronetLoader : CronetEngine.Builder.LibraryLoader(), Cronet.LoaderInterface {
|
||||
//https://storage.googleapis.com/chromium-cronet/android/92.0.4515.159/Release/cronet/libs/arm64-v8a/libcronet.92.0.4515.159.so
|
||||
|
||||
private const val soVersion = BuildConfig.Cronet_Version
|
||||
private const val soName = "libcronet.$soVersion.so"
|
||||
private val soUrl: String
|
||||
private val soFile: File
|
||||
private val downloadFile: File
|
||||
private var cpuAbi: String? = null
|
||||
private var md5: String
|
||||
var download = false
|
||||
|
||||
@Volatile
|
||||
private var cacheInstall = false
|
||||
|
||||
init {
|
||||
soUrl = ("https://storage.googleapis.com/chromium-cronet/android/"
|
||||
+ soVersion + "/Release/cronet/libs/"
|
||||
+ getCpuAbi(appCtx) + "/" + soName)
|
||||
md5 = getMd5(appCtx)
|
||||
val dir = appCtx.getDir("cronet", Context.MODE_PRIVATE)
|
||||
soFile = File(dir.toString() + "/" + getCpuAbi(appCtx), soName)
|
||||
downloadFile = File(appCtx.cacheDir.toString() + "/so_download", soName)
|
||||
DebugLog.d(javaClass.simpleName, "soName+:$soName")
|
||||
DebugLog.d(javaClass.simpleName, "destSuccessFile:$soFile")
|
||||
DebugLog.d(javaClass.simpleName, "tempFile:$downloadFile")
|
||||
DebugLog.d(javaClass.simpleName, "soUrl:$soUrl")
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断Cronet是否安装完成
|
||||
*/
|
||||
override fun install(): Boolean {
|
||||
synchronized(this) {
|
||||
if (cacheInstall) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (md5.length != 32 || !soFile.exists() || md5 != getFileMD5(soFile)) {
|
||||
cacheInstall = false
|
||||
return cacheInstall
|
||||
}
|
||||
cacheInstall = soFile.exists()
|
||||
return cacheInstall
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 预加载Cronet
|
||||
*/
|
||||
override fun preDownload() {
|
||||
Coroutine.async {
|
||||
//md5 = getUrlMd5(md5Url)
|
||||
if (soFile.exists() && md5 == getFileMD5(soFile)) {
|
||||
DebugLog.d(javaClass.simpleName, "So 库已存在")
|
||||
} else {
|
||||
download(soUrl, md5, downloadFile, soFile)
|
||||
}
|
||||
DebugLog.d(javaClass.simpleName, soName)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMd5(context: Context): String {
|
||||
val stringBuilder = StringBuilder()
|
||||
return try {
|
||||
//获取assets资源管理器
|
||||
val assetManager = context.assets
|
||||
//通过管理器打开文件并读取
|
||||
val bf = BufferedReader(
|
||||
InputStreamReader(
|
||||
assetManager.open("cronet.json")
|
||||
)
|
||||
)
|
||||
var line: String?
|
||||
while (bf.readLine().also { line = it } != null) {
|
||||
stringBuilder.append(line)
|
||||
}
|
||||
JSONObject(stringBuilder.toString()).optString(getCpuAbi(context), "")
|
||||
} catch (e: java.lang.Exception) {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||
override fun loadLibrary(libName: String) {
|
||||
DebugLog.d(javaClass.simpleName, "libName:$libName")
|
||||
val start = System.currentTimeMillis()
|
||||
@Suppress("SameParameterValue")
|
||||
try {
|
||||
//非cronet的so调用系统方法加载
|
||||
if (!libName.contains("cronet")) {
|
||||
System.loadLibrary(libName)
|
||||
return
|
||||
}
|
||||
//以下逻辑为cronet加载,优先加载本地,否则从远程加载
|
||||
//首先调用系统行为进行加载
|
||||
System.loadLibrary(libName)
|
||||
DebugLog.d(javaClass.simpleName, "load from system")
|
||||
} catch (e: Throwable) {
|
||||
//如果找不到,则从远程下载
|
||||
//删除历史文件
|
||||
deleteHistoryFile(Objects.requireNonNull(soFile.parentFile), soFile)
|
||||
//md5 = getUrlMd5(md5Url)
|
||||
DebugLog.d(javaClass.simpleName, "soMD5:$md5")
|
||||
if (md5.length != 32 || soUrl.isEmpty()) {
|
||||
//如果md5或下载的url为空,则调用系统行为进行加载
|
||||
System.loadLibrary(libName)
|
||||
return
|
||||
}
|
||||
if (!soFile.exists() || !soFile.isFile) {
|
||||
soFile.delete()
|
||||
download(soUrl, md5, downloadFile, soFile)
|
||||
//如果文件不存在或不是文件,则调用系统行为进行加载
|
||||
System.loadLibrary(libName)
|
||||
return
|
||||
}
|
||||
if (soFile.exists()) {
|
||||
//如果文件存在,则校验md5值
|
||||
val fileMD5 = getFileMD5(soFile)
|
||||
if (fileMD5 != null && fileMD5.equals(md5, ignoreCase = true)) {
|
||||
//md5值一样,则加载
|
||||
System.load(soFile.absolutePath)
|
||||
DebugLog.d(javaClass.simpleName, "load from:$soFile")
|
||||
return
|
||||
}
|
||||
//md5不一样则删除
|
||||
soFile.delete()
|
||||
}
|
||||
//不存在则下载
|
||||
download(soUrl, md5, downloadFile, soFile)
|
||||
//使用系统加载方法
|
||||
System.loadLibrary(libName)
|
||||
} finally {
|
||||
DebugLog.d(javaClass.simpleName, "time:" + (System.currentTimeMillis() - start))
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DiscouragedPrivateApi")
|
||||
private fun getCpuAbi(context: Context): String? {
|
||||
if (cpuAbi != null) {
|
||||
return cpuAbi
|
||||
}
|
||||
// 5.0以上Application才有primaryCpuAbi字段
|
||||
try {
|
||||
val appInfo = context.applicationInfo
|
||||
val abiField = ApplicationInfo::class.java.getDeclaredField("primaryCpuAbi")
|
||||
abiField.isAccessible = true
|
||||
cpuAbi = abiField.get(appInfo) as String?
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
if (TextUtils.isEmpty(cpuAbi)) {
|
||||
cpuAbi = Build.SUPPORTED_ABIS[0]
|
||||
}
|
||||
return cpuAbi
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除历史文件
|
||||
*/
|
||||
private fun deleteHistoryFile(dir: File, currentFile: File?) {
|
||||
val files = dir.listFiles()
|
||||
@Suppress("SameParameterValue")
|
||||
if (files != null && files.isNotEmpty()) {
|
||||
for (f in files) {
|
||||
if (f.exists() && (currentFile == null || f.absolutePath != currentFile.absolutePath)) {
|
||||
val delete = f.delete()
|
||||
DebugLog.d(javaClass.simpleName, "delete file: $f result: $delete")
|
||||
if (!delete) {
|
||||
f.deleteOnExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*/
|
||||
private fun downloadFileIfNotExist(url: String, destFile: File): Boolean {
|
||||
var inputStream: InputStream? = null
|
||||
var outputStream: OutputStream? = null
|
||||
try {
|
||||
val connection = URL(url).openConnection() as HttpURLConnection
|
||||
inputStream = connection.inputStream
|
||||
if (destFile.exists()) {
|
||||
return true
|
||||
}
|
||||
destFile.parentFile!!.mkdirs()
|
||||
destFile.createNewFile()
|
||||
outputStream = FileOutputStream(destFile)
|
||||
val buffer = ByteArray(32768)
|
||||
var read: Int
|
||||
while (inputStream.read(buffer).also { read = it } != -1) {
|
||||
outputStream.write(buffer, 0, read)
|
||||
outputStream.flush()
|
||||
}
|
||||
return true
|
||||
} catch (e: Throwable) {
|
||||
e.printOnDebug()
|
||||
if (destFile.exists() && !destFile.delete()) {
|
||||
destFile.deleteOnExit()
|
||||
}
|
||||
} finally {
|
||||
if (inputStream != null) {
|
||||
try {
|
||||
inputStream.close()
|
||||
} catch (e: IOException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close()
|
||||
} catch (e: IOException) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载并拷贝文件
|
||||
*/
|
||||
@Suppress("SameParameterValue")
|
||||
@Synchronized
|
||||
private fun download(
|
||||
url: String,
|
||||
md5: String?,
|
||||
downloadTempFile: File,
|
||||
destSuccessFile: File
|
||||
) {
|
||||
if (download) {
|
||||
return
|
||||
}
|
||||
download = true
|
||||
|
||||
Coroutine.async {
|
||||
val result = downloadFileIfNotExist(url, downloadTempFile)
|
||||
DebugLog.d(javaClass.simpleName, "download result:$result")
|
||||
//文件md5再次校验
|
||||
val fileMD5 = getFileMD5(downloadTempFile)
|
||||
if (md5 != null && !md5.equals(fileMD5, ignoreCase = true)) {
|
||||
val delete = downloadTempFile.delete()
|
||||
if (!delete) {
|
||||
downloadTempFile.deleteOnExit()
|
||||
}
|
||||
download = false
|
||||
return@async
|
||||
}
|
||||
DebugLog.d(javaClass.simpleName, "download success, copy to $destSuccessFile")
|
||||
//下载成功拷贝文件
|
||||
copyFile(downloadTempFile, destSuccessFile)
|
||||
cacheInstall = false
|
||||
val parentFile = downloadTempFile.parentFile
|
||||
@Suppress("SameParameterValue")
|
||||
(deleteHistoryFile(parentFile!!, null))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝文件
|
||||
*/
|
||||
private fun copyFile(source: File?, dest: File?): Boolean {
|
||||
if (source == null || !source.exists() || !source.isFile || dest == null) {
|
||||
return false
|
||||
}
|
||||
if (source.absolutePath == dest.absolutePath) {
|
||||
return true
|
||||
}
|
||||
var fileInputStream: FileInputStream? = null
|
||||
var os: FileOutputStream? = null
|
||||
val parent = dest.parentFile
|
||||
if (parent != null && !parent.exists()) {
|
||||
val mkdirs = parent.mkdirs()
|
||||
if (!mkdirs) {
|
||||
parent.mkdirs()
|
||||
}
|
||||
}
|
||||
try {
|
||||
fileInputStream = FileInputStream(source)
|
||||
os = FileOutputStream(dest, false)
|
||||
val buffer = ByteArray(1024 * 512)
|
||||
var length: Int
|
||||
while (fileInputStream.read(buffer).also { length = it } > 0) {
|
||||
os.write(buffer, 0, length)
|
||||
}
|
||||
return true
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
if (fileInputStream != null) {
|
||||
try {
|
||||
fileInputStream.close()
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
if (os != null) {
|
||||
try {
|
||||
os.close()
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得文件md5
|
||||
*/
|
||||
private fun getFileMD5(file: File): String? {
|
||||
var fileInputStream: FileInputStream? = null
|
||||
try {
|
||||
fileInputStream = FileInputStream(file)
|
||||
val md5 = MessageDigest.getInstance("MD5")
|
||||
val buffer = ByteArray(1024)
|
||||
var numRead: Int
|
||||
while (fileInputStream.read(buffer).also { numRead = it } > 0) {
|
||||
md5.update(buffer, 0, numRead)
|
||||
}
|
||||
return String.format("%032x", BigInteger(1, md5.digest())).lowercase()
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
} catch (e: OutOfMemoryError) {
|
||||
e.printOnDebug()
|
||||
} finally {
|
||||
if (fileInputStream != null) {
|
||||
try {
|
||||
fileInputStream.close()
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import okhttp3.RequestBody
|
||||
import okio.BufferedSource
|
||||
import okio.Pipe
|
||||
import okio.buffer
|
||||
import org.chromium.net.UploadDataProvider
|
||||
import org.chromium.net.UploadDataSink
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.concurrent.ExecutorService
|
||||
|
||||
/**
|
||||
* 用于上传大型文件
|
||||
*
|
||||
* @property body
|
||||
* @property executorService
|
||||
*/
|
||||
@Keep
|
||||
class LargeBodyUploadProvider(
|
||||
private val body: RequestBody,
|
||||
private val executorService: ExecutorService
|
||||
) : UploadDataProvider(), AutoCloseable {
|
||||
private val pipe = Pipe(BUFFER_SIZE.toLong())
|
||||
private var source: BufferedSource = pipe.source.buffer()
|
||||
|
||||
@Volatile
|
||||
private var filled: Boolean = false
|
||||
override fun getLength(): Long {
|
||||
return body.contentLength()
|
||||
}
|
||||
|
||||
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
|
||||
if (!filled) {
|
||||
fillBuffer()
|
||||
}
|
||||
check(byteBuffer.hasRemaining()) { "Cronet passed a buffer with no bytes remaining" }
|
||||
var read: Int
|
||||
var bytesRead = 0
|
||||
while (bytesRead <= 0) {
|
||||
read = source.read(byteBuffer)
|
||||
bytesRead += read
|
||||
}
|
||||
uploadDataSink.onReadSucceeded(false)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun fillBuffer() {
|
||||
executorService.submit {
|
||||
try {
|
||||
val writeSink = pipe.sink.buffer()
|
||||
filled = true
|
||||
body.writeTo(writeSink)
|
||||
writeSink.flush()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun rewind(p0: UploadDataSink?) {
|
||||
check(body.isOneShot()) { "Okhttp RequestBody is OneShot" }
|
||||
filled = false
|
||||
fillBuffer()
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
// pipe.cancel()
|
||||
// source.close()
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Build
|
||||
import androidx.annotation.Keep
|
||||
import androidx.annotation.RequiresApi
|
||||
import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.chromium.net.UrlRequest
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
@Keep
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
class NewCallBack(originalRequest: Request, mCall: Call) : AbsCallBack(originalRequest, mCall) {
|
||||
|
||||
private val responseFuture = CompletableFuture<Response>()
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun waitForDone(urlRequest: UrlRequest): Response {
|
||||
urlRequest.start()
|
||||
//DebugLog.i(javaClass.simpleName, "start ${originalRequest.method} ${originalRequest.url}")
|
||||
return if (mCall.timeout().timeoutNanos() > 0) {
|
||||
responseFuture.get(mCall.timeout().timeoutNanos(), TimeUnit.NANOSECONDS)
|
||||
} else {
|
||||
return responseFuture.get()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当发生错误时,通知子类终止阻塞抛出错误
|
||||
* @param error
|
||||
*/
|
||||
override fun onError(error: IOException) {
|
||||
responseFuture.completeExceptionally(error)
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求成功后,通知子类结束阻塞,返回response
|
||||
* @param response
|
||||
*/
|
||||
override fun onSuccess(response: Response) {
|
||||
responseFuture.complete(response)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.legado.app.lib.cronet
|
||||
|
||||
import android.os.ConditionVariable
|
||||
import androidx.annotation.Keep
|
||||
import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.chromium.net.UrlRequest
|
||||
import java.io.IOException
|
||||
|
||||
@Keep
|
||||
class OldCallback(originalRequest: Request, mCall: Call) : AbsCallBack(originalRequest, mCall) {
|
||||
|
||||
private val mResponseCondition = ConditionVariable()
|
||||
private var mException: IOException? = null
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun waitForDone(urlRequest: UrlRequest): Response {
|
||||
//获取okhttp call的完整请求的超时时间
|
||||
val timeOutMs: Long = mCall.timeout().timeoutNanos() / 1000000
|
||||
urlRequest.start()
|
||||
if (timeOutMs > 0) {
|
||||
mResponseCondition.block(timeOutMs)
|
||||
} else {
|
||||
mResponseCondition.block()
|
||||
}
|
||||
//ConditionVariable 正常open或者超时open后,检查urlRequest是否完成
|
||||
if (!urlRequest.isDone) {
|
||||
urlRequest.cancel()
|
||||
mException = IOException("Cronet timeout after wait " + timeOutMs + "ms")
|
||||
}
|
||||
|
||||
if (mException != null) {
|
||||
throw mException as IOException
|
||||
}
|
||||
return mResponse
|
||||
}
|
||||
|
||||
/**
|
||||
* 当发生错误时,通知子类终止阻塞抛出错误
|
||||
* @param error
|
||||
*/
|
||||
override fun onError(error: IOException) {
|
||||
mException = error
|
||||
mResponseCondition.open()
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求成功后,通知子类结束阻塞,返回response
|
||||
* @param response
|
||||
*/
|
||||
override fun onSuccess(response: Response) {
|
||||
mResponseCondition.open()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user