优化
This commit is contained in:
@@ -24,7 +24,7 @@ import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ThemeConfig.applyDayNight
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.http.cronet.CronetLoader
|
||||
import io.legado.app.help.http.cronet.Cronet
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.utils.defaultSharedPreferences
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
@@ -42,7 +42,7 @@ class App : MultiDexApplication() {
|
||||
oldConfig = Configuration(resources.configuration)
|
||||
CrashHandler(this)
|
||||
//预下载Cronet so
|
||||
CronetLoader.preDownload()
|
||||
Cronet.preDownload()
|
||||
createNotificationChannels()
|
||||
applyDayNight(this)
|
||||
LiveEventBus.config()
|
||||
|
||||
@@ -3,8 +3,7 @@ package io.legado.app.help.http
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.help.CacheManager
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.http.cronet.CronetInterceptor
|
||||
import io.legado.app.help.http.cronet.CronetLoader
|
||||
import io.legado.app.help.http.cronet.Cronet
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
import okhttp3.*
|
||||
import java.net.InetSocketAddress
|
||||
@@ -70,8 +69,12 @@ val okHttpClient: OkHttpClient by lazy {
|
||||
builder.addHeader("Cache-Control", "no-cache")
|
||||
chain.proceed(builder.build())
|
||||
})
|
||||
if (!AppConfig.isGooglePlay && AppConfig.isCronet && CronetLoader.install()) {
|
||||
builder.addInterceptor(CronetInterceptor(cookieJar = cookieJar))
|
||||
if (!AppConfig.isGooglePlay && AppConfig.isCronet) {
|
||||
if (Cronet.loader?.install() == true) {
|
||||
Cronet.interceptor?.let {
|
||||
builder.addInterceptor(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
|
||||
@@ -1,232 +0,0 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.utils.DebugLog
|
||||
import io.legado.app.utils.asIOException
|
||||
import okhttp3.*
|
||||
import okhttp3.EventListener
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||
import okio.Buffer
|
||||
import org.chromium.net.CronetException
|
||||
import org.chromium.net.UrlRequest
|
||||
import org.chromium.net.UrlResponseInfo
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
|
||||
abstract class AbsCallBack(
|
||||
val originalRequest: Request,
|
||||
val mCall: Call,
|
||||
private val eventListener: EventListener? = null,
|
||||
private val responseCallback: Callback? = null
|
||||
|
||||
) : UrlRequest.Callback(), AutoCloseable {
|
||||
|
||||
val buffer = Buffer()
|
||||
|
||||
var mResponse: Response
|
||||
|
||||
private var followCount = 0
|
||||
|
||||
|
||||
@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
|
||||
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 (okHttpClient.followRedirects) {
|
||||
request.followRedirect()
|
||||
} else {
|
||||
onError(IOException("Too many redirect"))
|
||||
request.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onResponseStarted(request: UrlRequest, info: UrlResponseInfo) {
|
||||
this.mResponse = responseFromResponse(this.mResponse, info)
|
||||
//打印协议,用于调试
|
||||
DebugLog.i(javaClass.simpleName, "start[${info.negotiatedProtocol}]${info.url}")
|
||||
if (eventListener != null) {
|
||||
eventListener.responseHeadersEnd(mCall, this.mResponse)
|
||||
eventListener.responseBodyStart(mCall)
|
||||
}
|
||||
request.read(ByteBuffer.allocateDirect(32 * 1024))
|
||||
}
|
||||
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun onReadCompleted(
|
||||
request: UrlRequest,
|
||||
info: UrlResponseInfo,
|
||||
byteBuffer: ByteBuffer
|
||||
) {
|
||||
|
||||
|
||||
if (mCall.isCanceled()) {
|
||||
request.cancel()
|
||||
onError(IOException("Request Canceled"))
|
||||
}
|
||||
|
||||
byteBuffer.flip()
|
||||
|
||||
try {
|
||||
buffer.write(byteBuffer)
|
||||
} catch (e: IOException) {
|
||||
DebugLog.e(javaClass.name, "IOException during ByteBuffer read. Details: ", e)
|
||||
onError(IOException("IOException during ByteBuffer read. Details:", e))
|
||||
return
|
||||
}
|
||||
byteBuffer.clear()
|
||||
request.read(byteBuffer)
|
||||
}
|
||||
|
||||
|
||||
override fun onSucceeded(request: UrlRequest, info: UrlResponseInfo) {
|
||||
eventListener?.responseBodyEnd(mCall, info.receivedByteCount)
|
||||
val contentType: MediaType? = (this.mResponse.header("content-type")
|
||||
?: "text/plain; charset=\"utf-8\"").toMediaTypeOrNull()
|
||||
val responseBody: ResponseBody =
|
||||
buffer.asResponseBody(contentType)
|
||||
val newRequest = originalRequest.newBuilder().url(info.url).build()
|
||||
this.mResponse = this.mResponse.newBuilder().body(responseBody).request(newRequest).build()
|
||||
onSuccess(this.mResponse)
|
||||
//DebugLog.i(javaClass.simpleName, "end[${info.negotiatedProtocol}]${info.url}")
|
||||
|
||||
eventListener?.callEnd(mCall)
|
||||
if (responseCallback != null) {
|
||||
try {
|
||||
responseCallback.onResponse(mCall, this.mResponse)
|
||||
} catch (e: IOException) {
|
||||
// Pass?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//UrlResponseInfo可能为null
|
||||
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
|
||||
DebugLog.e(javaClass.name, error.message.toString())
|
||||
onError(error.asIOException())
|
||||
this.eventListener?.callFailed(mCall, error)
|
||||
responseCallback?.onFailure(mCall, error)
|
||||
}
|
||||
|
||||
override fun onCanceled(request: UrlRequest?, info: UrlResponseInfo?) {
|
||||
super.onCanceled(request, info)
|
||||
this.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 fun protocolFromNegotiatedProtocol(responseInfo: UrlResponseInfo): Protocol {
|
||||
val negotiatedProtocol = responseInfo.negotiatedProtocol.lowercase(Locale.getDefault())
|
||||
return when {
|
||||
negotiatedProtocol.contains("h3") -> {
|
||||
return 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): Headers {
|
||||
val headers = responseInfo.allHeadersAsList
|
||||
return Headers.Builder().apply {
|
||||
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
|
||||
}
|
||||
add(key, value)
|
||||
} catch (e: Exception) {
|
||||
DebugLog.w(javaClass.name, "Invalid HTTP header/value: $key$value")
|
||||
// Ignore that header
|
||||
}
|
||||
}
|
||||
|
||||
}.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()
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
buffer.clear()
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import okhttp3.RequestBody
|
||||
import okio.Buffer
|
||||
import org.chromium.net.UploadDataProvider
|
||||
import org.chromium.net.UploadDataSink
|
||||
import java.io.IOException
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
class BodyUploadProvider(private val body: RequestBody) : UploadDataProvider(), AutoCloseable {
|
||||
private val buffer = Buffer()
|
||||
|
||||
init {
|
||||
try {
|
||||
body.writeTo(buffer)
|
||||
} 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) {
|
||||
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) {
|
||||
buffer.clear()
|
||||
body.writeTo(buffer)
|
||||
uploadDataSink.onRewindSucceeded()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun close() {
|
||||
buffer.close()
|
||||
super.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import okhttp3.Interceptor
|
||||
|
||||
object Cronet {
|
||||
|
||||
val loader: CronetLoaderInterface? by lazy {
|
||||
val cl = Class.forName("io.legado.app.lib.cronet.CronetLoader")
|
||||
?.kotlin?.objectInstance
|
||||
cl as? CronetLoaderInterface
|
||||
}
|
||||
|
||||
fun preDownload() {
|
||||
loader?.preDownload()
|
||||
}
|
||||
|
||||
val interceptor: Interceptor? by lazy {
|
||||
val cl = Class.forName("io.legado.app.lib.cronet.CronetInterceptor")?.newInstance()
|
||||
cl as? Interceptor
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
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
|
||||
|
||||
class CronetCoroutineInterceptor(private val cookieJar: CookieJar = CookieJar.NO_COOKIES) :
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
buildRequest(request, callBack)?.start()
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.help.config.AppConfig
|
||||
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.UrlRequest
|
||||
import org.json.JSONObject
|
||||
import splitties.init.appCtx
|
||||
|
||||
|
||||
val cronetEngine: ExperimentalCronetEngine? by lazy {
|
||||
if (!AppConfig.isGooglePlay) {
|
||||
CronetLoader.preDownload()
|
||||
}
|
||||
val builder = ExperimentalCronetEngine.Builder(appCtx).apply {
|
||||
if (!AppConfig.isGooglePlay && 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: UnsatisfiedLinkError) {
|
||||
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, _ ->
|
||||
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")
|
||||
}
|
||||
setUploadDataProvider(
|
||||
BodyUploadProvider(requestBody),
|
||||
okHttpClient.dispatcher.executorService
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}?.build()
|
||||
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import android.os.Build
|
||||
import io.legado.app.utils.printOnDebug
|
||||
import okhttp3.*
|
||||
import okhttp3.internal.http.receiveHeaders
|
||||
import java.io.IOException
|
||||
|
||||
|
||||
class CronetInterceptor(private val cookieJar: CookieJar = CookieJar.NO_COOKIES) : 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")
|
||||
if (cookieJar != CookieJar.NO_COOKIES) {
|
||||
val cookieStr = getCookie(original.url)
|
||||
//设置Cookie
|
||||
if (cookieStr.length > 3) {
|
||||
builder.addHeader("Cookie", cookieStr)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,377 +0,0 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.os.Build
|
||||
import android.text.TextUtils
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
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.*
|
||||
|
||||
|
||||
object CronetLoader : CronetEngine.Builder.LibraryLoader() {
|
||||
//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是否安装完成
|
||||
*/
|
||||
fun install(): Boolean {
|
||||
synchronized(this) {
|
||||
if (cacheInstall) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if (AppConfig.isGooglePlay) {
|
||||
return false
|
||||
}
|
||||
if (md5.length != 32 || !soFile.exists() || md5 != getFileMD5(soFile)) {
|
||||
cacheInstall = false
|
||||
return cacheInstall
|
||||
}
|
||||
cacheInstall = soFile.exists()
|
||||
return cacheInstall
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 预加载Cronet
|
||||
*/
|
||||
fun preDownload() {
|
||||
if (AppConfig.isGooglePlay) {
|
||||
return
|
||||
}
|
||||
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 || AppConfig.isGooglePlay) {
|
||||
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)
|
||||
}
|
||||
// executor.execute {
|
||||
//
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 拷贝文件
|
||||
*/
|
||||
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,9 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
interface CronetLoaderInterface {
|
||||
|
||||
fun install(): Boolean
|
||||
|
||||
fun preDownload()
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
|
||||
import android.os.Build
|
||||
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
|
||||
|
||||
@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()
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package io.legado.app.help.http.cronet
|
||||
|
||||
import android.os.ConditionVariable
|
||||
import okhttp3.Call
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import org.chromium.net.UrlRequest
|
||||
import java.io.IOException
|
||||
|
||||
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