This commit is contained in:
kunfei
2023-01-22 21:58:19 +08:00
parent 57f696dc06
commit d3f2ebd538
13 changed files with 60 additions and 22 deletions
+2 -2
View File
@@ -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()
}
}