This commit is contained in:
kunfei
2023-09-21 19:46:00 +08:00
parent 3908d31347
commit 904b98f565
12 changed files with 0 additions and 8 deletions
@@ -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()
}
}